48 KiB
48 KiB
海洋项目管理系统 - 产品交互文档(前端开发版)
文档说明
本文档面向前端开发人员,详细描述系统各功能模块的交互流程、状态管理、API调用时机和实现细节。
版本: v1.0 更新日期: 2026-01-25 适用系统版本: v1.0
目录
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;
}
筛选交互:
- 选择筛选条件
- 点击"筛选"按钮或按Enter键
- 调用API获取筛选后的数据
- 更新表格显示
清空筛选:
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
加油!!!