Files
ocean_project_manager/docs/desigen/product-interaction-document.md
2026-01-26 08:04:53 +08:00

48 KiB

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

文档说明

本文档面向前端开发人员,详细描述系统各功能模块的交互流程、状态管理、API调用时机和实现细节。

版本: v1.0 更新日期: 2026-01-25 适用系统版本: v1.0


目录

  1. 系统整体架构
  2. 认证与授权
  3. 页面导航与路由
  4. 登录模块
  5. 仪表盘模块
  6. 项目管理模块
  7. 用户管理模块
  8. 项目统计模块
  9. 通用组件与工具
  10. 状态管理
  11. 错误处理
  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

请求拦截器:

// utils/request.ts
axios.interceptors.request.use((config) => {
  // 添加Token
  const token = localStorage.getItem('token');
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
});

响应拦截器:

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结构:

interface AuthToken {
  token: string;
  token_type: string;
  user: UserInfo;
}

Token过期处理:

  • Token有效期:24小时
  • 每次API调用检查Token是否过期
  • 过期时自动跳转登录页

2.3 权限控制

权限类型:

  • admin: 管理员
  • market: 市场部
  • other: 其他部门

权限检查Hook:

// 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 };
};

页面级权限:

// 路由守卫
const ProtectedRoute = ({ children, requiredRole }) => {
  const { user } = useAuth();
  if (!user) return <Navigate to="/login" />;
  if (requiredRole && !hasPermission(user.role, requiredRole)) {
    return <div>您没有访问该页面的权限</div>;
  }
  return children;
};

组件级权限:

const { hasPermission } = useAuth();
{hasPermission('admin') && <Button>删除用户</Button>}

3. 页面导航与路由

3.1 路由配置

// 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 面包屑导航

实现方式:

// components/Breadcrumb.tsx
const Breadcrumb = () => {
  const location = useLocation();
  const breadcrumbItems = generateBreadcrumb(location.pathname);
  return <AntdBreadcrumb items={breadcrumbItems} />;
};

面包屑数据结构:

interface BreadcrumbItem {
  label: string;
  path?: string;
}

4. 登录模块

4.1 页面交互流程

┌─────────────────────────────────────────────┐
│  1. 用户输入用户名和密码                   │
└────────────┬──────────────────────────────┘
             │
             ▼
┌─────────────────────────────────────────────┐
│  2. 前端验证:检查必填字段                 │
│     - 用户名不能为空                        │
│     - 密码不能为空                          │
└────────────┬──────────────────────────────┘
             │ 验证通过
             ▼
┌─────────────────────────────────────────────┐
│  3. 调用登录API: POST /auth/login        │
│     请求体: { username, password }        │
└────────────┬──────────────────────────────┘
             │
             ▼
┌─────────────────────────────────────────────┐
│  4. API响应处理                          │
│     - 成功: 存储Token,跳转首页            │
│     - 失败: 显示错误提示                   │
└─────────────────────────────────────────────┘

4.2 表单验证规则

// utils/validate.ts
export const loginValidationSchema = {
  username: {
    required: '请输入用户名',
    minLength: { value: 3, message: '用户名至少3个字符' },
  },
  password: {
    required: '请输入密码',
    minLength: { value: 6, message: '密码至少6个字符' },
  },
};

4.3 记住密码功能

实现逻辑:

// 本地存储加密的密码(可选)
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调用

// api/auth.ts
export const login = (data: LoginRequest) => {
  return axios.post('/auth/login', data);
};

请求参数:

interface LoginRequest {
  username: string;
  password: string;
}

响应数据:

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调用:

// api/statistics.ts
export const getBasicStatistics = (params?: StatisticsFilter) => {
  return axios.get('/projects/statistics', { params });
};

响应数据:

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 统计卡片渲染

卡片数据映射:

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 项目趋势图

数据获取:

export const getTimelineStatistics = (params: TimelineParams) => {
  return axios.get('/projects/statistics/timeline', { params });
};

图表配置:

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 最近项目列表

数据获取:

export const getRecentProjects = () => {
  return axios.get('/projects', {
    params: {
      page: 1,
      page_size: 5,
      sort_by: 'created_at',
      sort_order: 'desc',
    },
  });
};

点击交互:

const handleProjectClick = (projectId: number) => {
  navigate(`/projects/${projectId}`);
};

5.6 刷新机制

自动刷新:

  • 每5分钟自动刷新统计数据
  • 用户手动刷新:点击刷新按钮

实现:

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 搜索功能

实时搜索:

// 防抖处理,避免频繁请求
const debouncedSearch = useDebounce(keyword, 500);

useEffect(() => {
  if (debouncedSearch) {
    fetchProjects({ keyword: debouncedSearch });
  }
}, [debouncedSearch]);

搜索字段:

  • 项目名称
  • 合同编号
  • 业主单位

6.1.3 筛选功能

筛选条件:

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. 更新表格显示

清空筛选:

const handleClearFilters = () => {
  setFilters({});
  fetchProjects({ page: 1 });
};

6.1.4 排序功能

点击表头排序:

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 分页功能

分页参数:

interface PaginationParams {
  page: number;
  page_size: number;
}

分页交互:

const handlePageChange = (page: number) => {
  fetchProjects({ page });
  // 滚动到表格顶部
  window.scrollTo({ top: 0, behavior: 'smooth' });
};

6.1.6 新建项目

按钮点击:

const handleCreateProject = () => {
  // 打开新建项目模态框
  setModalVisible(true);
  setModalMode('create');
};

模态框表单:

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 查看项目详情

点击"查看"按钮:

const handleViewProject = (projectId: number) => {
  navigate(`/projects/${projectId}`);
};

6.1.8 编辑项目

点击"编辑"按钮:

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 删除项目

点击"删除"按钮:

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调用:

export const getProjectDetail = (id: number) => {
  return axios.get(`/projects/${id}`);
};

6.2.3 信息分组显示

分组配置:

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 编辑项目

权限控制:

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 删除项目

二次确认:

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 返回列表

const handleBack = () => {
  navigate('/projects');
};

6.3 项目表单

6.3.1 表单字段验证

必填字段:

const requiredFields = {
  project_no: '合同编号',
  name: '项目名称',
  engineering_type: '工程类别',
  contract_amount: '合同金额',
};

验证规则:

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 表单提交

提交前验证:

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 表单字段分组

分组配置:

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 动态字段显示

根据角色显示/隐藏字段:

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 权限控制

页面级权限:

// 只有管理员可以访问用户管理页面
const UserManagementPage = () => {
  const { user, hasPermission } = useAuth();

  if (!hasPermission('admin')) {
    return <div>您没有访问该页面的权限</div>;
  }

  return <UserList />;
};

7.2 用户列表

7.2.1 数据获取

API调用:

export const getUsers = (params: UserListParams) => {
  return axios.get('/users', { params });
};

查询参数:

interface UserListParams {
  page: number;
  page_size: number;
  department?: string;
  role?: string;
  keyword?: string;
}

7.2.2 搜索用户

搜索字段:

  • 用户名
  • 真实姓名
  • 邮箱

7.2.3 筛选用户

筛选条件:

  • 部门
  • 角色

7.3 新建用户

7.3.1 表单字段

interface CreateUserForm {
  username: string;      // 必填,唯一
  password: string;      // 必填,最少6位
  real_name: string;     // 必填
  department: string;    // 必填
  role: string;          // 必填:admin/market/other
  email?: string;        // 可选,唯一
  phone?: string;        // 可选
}

7.3.2 表单验证

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 提交处理

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 加载用户信息

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 用户名只读

<Form.Item
  name="username"
  label="用户名"
  rules={userValidationRules.username}
>
  <Input disabled={mode === 'edit'} />
</Form.Item>

7.5 删除用户

const handleDeleteUser = (userId: number) => {
  Modal.confirm({
    title: '确认删除',
    content: '确定要删除该用户吗?',
    onOk: async () => {
      try {
        await deleteUser(userId);
        message.success('用户删除成功');
        refetchUsers();
      } catch (error) {
        message.error('删除失败,请重试');
      }
    },
  });
};

7.6 重置密码

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 数据获取

export const getBasicStatistics = (filters?: StatisticsFilter) => {
  return axios.get('/projects/statistics', { params: filters });
};

8.1.2 统计卡片渲染

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 数据获取

export const getGroupStatistics = (params: GroupStatisticsParams) => {
  return axios.get('/projects/statistics/group', { params });
};

参数:

interface GroupStatisticsParams {
  group_by: 'engineering_type' | 'project_department';
  signing_date_start?: string;
  signing_date_end?: string;
}

8.2.2 饼图渲染

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 柱状图渲染

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 数据获取

export const getTimelineStatistics = (params: TimelineParams) => {
  return axios.get('/projects/statistics/timeline', { params });
};

参数:

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 折线图渲染

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 筛选条件

interface StatisticsFilter {
  engineering_type?: string;
  signing_date_start?: string;
  signing_date_end?: string;
  contract_amount_min?: number;
  contract_amount_max?: number;
}

8.4.2 筛选交互

const handleFilterChange = (filters: StatisticsFilter) => {
  setFilters(filters);
  // 重新获取统计数据
  refetchStatistics(filters);
};

8.5 导出报表

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 顶部导航栏

// 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 侧边栏

// 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 可配置表格

// 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 自定义列渲染

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 日期选择器

<Form.Item
  name="signing_date"
  label="签订日期"
  rules={[{ required: true, message: '请选择签订日期' }]}
>
  <DatePicker
    style={{ width: '100%' }}
    format="YYYY-MM-DD"
    placeholder="请选择日期"
  />
</Form.Item>

9.3.2 数字输入框

<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 标签组件

// 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配置

// 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 认证状态管理

// 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 项目状态管理

// 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集成

// 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错误处理

// 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 表单验证错误

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 加载错误处理

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+条)

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 防抖和节流

防抖:搜索输入

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)} />;
};

节流:滚动加载

import { useThrottle } from 'usehooks-ts';

const InfiniteScroll = () => {
  const handleScroll = useThrottle(() => {
    if (isNearBottom()) {
      loadMore();
    }
  }, 200);

  return <div onScroll={handleScroll}>...</div>;
};

12.3 懒加载

路由懒加载:

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 图片懒加载

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缓存:

// 默认缓存时间: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. 常用格式化函数

// 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类型定义

// 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;
  // ... 其他字段
}
// 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;
}
// 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

加油!!!