@@ -129,12 +133,26 @@ const CacheMonitorPage = () => {
-
+
+
+
-
+
+
+
diff --git a/src/pages/monitor/JobMonitorPage.tsx b/src/pages/monitor/JobMonitorPage.tsx
index 70ff843..8fcbd76 100644
--- a/src/pages/monitor/JobMonitorPage.tsx
+++ b/src/pages/monitor/JobMonitorPage.tsx
@@ -1,6 +1,7 @@
import { useCallback, useEffect, useState } from 'react';
import type { Key } from 'react';
import {
+ App,
Table,
Form,
Input,
@@ -8,7 +9,6 @@ import {
Button,
Dropdown,
Modal,
- message,
Space,
Tag,
Switch,
@@ -66,7 +66,41 @@ const normalizeRowKey = (value: Key): string | number => {
return typeof value === 'bigint' ? value.toString() : value;
};
+const inflightJobListRequests = new Map
>>>();
+const inflightJobDetailRequests = new Map>>>();
+
+const serializeJobQuery = (query: JobQueryParams) => JSON.stringify(query);
+
+const fetchJobList = async (query: JobQueryParams) => {
+ const requestKey = serializeJobQuery(query);
+ const existingRequest = inflightJobListRequests.get(requestKey);
+ if (existingRequest) {
+ return existingRequest;
+ }
+
+ const requestPromise = listJob(query).finally(() => {
+ inflightJobListRequests.delete(requestKey);
+ });
+ inflightJobListRequests.set(requestKey, requestPromise);
+ return requestPromise;
+};
+
+const fetchJobDetail = async (jobId: string | number) => {
+ const requestKey = String(jobId);
+ const existingRequest = inflightJobDetailRequests.get(requestKey);
+ if (existingRequest) {
+ return existingRequest;
+ }
+
+ const requestPromise = getJob(jobId).finally(() => {
+ inflightJobDetailRequests.delete(requestKey);
+ });
+ inflightJobDetailRequests.set(requestKey, requestPromise);
+ return requestPromise;
+};
+
const JobMonitorPage = () => {
+ const { message, modal } = App.useApp();
const [form] = Form.useForm();
const [queryForm] = Form.useForm();
const [jobList, setJobList] = useState([]);
@@ -90,7 +124,7 @@ const JobMonitorPage = () => {
const getList = useCallback(async () => {
setLoading(true);
try {
- const response = await listJob(queryParams);
+ const response = await fetchJobList(queryParams);
setJobList(response.rows ?? []);
setTotal(Number(response.total ?? 0));
} catch (error: unknown) {
@@ -99,7 +133,7 @@ const JobMonitorPage = () => {
} finally {
setLoading(false);
}
- }, [queryParams]);
+ }, [message, queryParams]);
useEffect(() => {
void getList();
@@ -136,7 +170,7 @@ const JobMonitorPage = () => {
}
try {
- const response = await getJob(jobId);
+ const response = await fetchJobDetail(jobId);
setCurrentJobDetail(response);
form.setFieldsValue({
...response,
@@ -159,7 +193,7 @@ const JobMonitorPage = () => {
return;
}
- Modal.confirm({
+ modal.confirm({
title: '确认删除',
content: `是否确认删除定时任务编号为"${jobIds.join(',')}"的数据项?`,
onOk: async () => {
@@ -208,7 +242,7 @@ const JobMonitorPage = () => {
}
const { jobId, jobGroup } = record;
- Modal.confirm({
+ modal.confirm({
title: '确认执行',
content: `确认要立即执行一次"${record.jobName ?? ''}"任务吗?`,
onOk: async () => {
@@ -225,7 +259,7 @@ const JobMonitorPage = () => {
const handleExport = async () => {
const hide = message.loading('正在导出数据...', 0);
try {
- const response = await listJob({ ...queryParams, pageNum: undefined, pageSize: undefined });
+ const response = await fetchJobList({ ...queryParams, pageNum: undefined, pageSize: undefined });
const header = ['任务编号', '任务名称', '任务组名', '调用目标字符串', 'cron执行表达式', '状态'];
const rows = response.rows.map((job) => [
job.jobId,
@@ -262,7 +296,7 @@ const JobMonitorPage = () => {
}
try {
- const response = await getJob(record.jobId);
+ const response = await fetchJobDetail(record.jobId);
setCurrentJobDetail(response);
setDetailModalVisible(true);
} catch {
diff --git a/src/pages/monitor/LoginLogPage.tsx b/src/pages/monitor/LoginLogPage.tsx
index 9b541d2..edf09f9 100644
--- a/src/pages/monitor/LoginLogPage.tsx
+++ b/src/pages/monitor/LoginLogPage.tsx
@@ -1,4 +1,4 @@
-import { useCallback, useEffect, useState } from 'react';
+import { useCallback, useEffect, useMemo, useState } from 'react';
import type { Key } from 'react';
import {
App,
@@ -52,6 +52,8 @@ const defaultQueryParams: LogininforQueryParams = {
isAsc: 'descending',
};
+const inflightLogininforRequests = new Map>>>();
+
const parseTime = (time?: string | number | Date, pattern = 'YYYY-MM-DD HH:mm:ss'): string => {
return time ? dayjs(time).format(pattern) : '';
};
@@ -61,6 +63,23 @@ const escapeCsvCell = (value: unknown): string => {
return `"${raw.replace(/"/g, '""')}"`;
};
+const serializeLogininforQuery = (query: LogininforQueryParams) => JSON.stringify(query);
+
+const fetchLogininforList = async (query: LogininforQueryParams) => {
+ const requestKey = serializeLogininforQuery(query);
+ const existingRequest = inflightLogininforRequests.get(requestKey);
+ if (existingRequest) {
+ return existingRequest;
+ }
+
+ const requestPromise = listLogininfor(query).finally(() => {
+ inflightLogininforRequests.delete(requestKey);
+ });
+
+ inflightLogininforRequests.set(requestKey, requestPromise);
+ return requestPromise;
+};
+
const LoginLogPage = () => {
const { message, modal } = App.useApp();
const [queryForm] = Form.useForm();
@@ -72,19 +91,22 @@ const LoginLogPage = () => {
const [dateRange, setDateRange] = useState(null);
const [queryParams, setQueryParams] = useState(defaultQueryParams);
+ const requestParams = useMemo(() => {
+ const formattedQueryParams: LogininforQueryParams = { ...queryParams };
+ if (dateRange?.[0] && dateRange?.[1]) {
+ formattedQueryParams.beginTime = dateRange[0].format('YYYY-MM-DD HH:mm:ss');
+ formattedQueryParams.endTime = dateRange[1].format('YYYY-MM-DD HH:mm:ss');
+ } else {
+ formattedQueryParams.beginTime = undefined;
+ formattedQueryParams.endTime = undefined;
+ }
+ return formattedQueryParams;
+ }, [dateRange, queryParams]);
+
const getList = useCallback(async () => {
setLoading(true);
try {
- const formattedQueryParams: LogininforQueryParams = { ...queryParams };
- if (dateRange?.[0] && dateRange?.[1]) {
- formattedQueryParams.beginTime = dateRange[0].format('YYYY-MM-DD HH:mm:ss');
- formattedQueryParams.endTime = dateRange[1].format('YYYY-MM-DD HH:mm:ss');
- } else {
- formattedQueryParams.beginTime = undefined;
- formattedQueryParams.endTime = undefined;
- }
-
- const response = await listLogininfor(formattedQueryParams);
+ const response = await fetchLogininforList(requestParams);
setList(response.rows ?? []);
setTotal(Number(response.total ?? 0));
} catch (error: unknown) {
@@ -93,7 +115,7 @@ const LoginLogPage = () => {
} finally {
setLoading(false);
}
- }, [dateRange, queryParams]);
+ }, [message, requestParams]);
useEffect(() => {
void getList();
@@ -186,8 +208,8 @@ const LoginLogPage = () => {
const handleExport = async () => {
const hide = message.loading('正在导出数据...', 0);
try {
- const response = await listLogininfor({
- ...queryParams,
+ const response = await fetchLogininforList({
+ ...requestParams,
pageNum: undefined,
pageSize: undefined,
});
diff --git a/src/pages/monitor/OnlineUserPage.tsx b/src/pages/monitor/OnlineUserPage.tsx
index 269d0da..caf2a0c 100644
--- a/src/pages/monitor/OnlineUserPage.tsx
+++ b/src/pages/monitor/OnlineUserPage.tsx
@@ -1,5 +1,5 @@
import { useCallback, useEffect, useState } from 'react';
-import { Table, Form, Input, Button, Modal, message } from 'antd';
+import { App, Table, Form, Input, Button } from 'antd';
import type { TableColumnsType } from 'antd';
import { SearchOutlined, ReloadOutlined, DeleteOutlined } from '@ant-design/icons';
import { listOnline, forceLogout } from '../../api/monitor/online';
@@ -17,6 +17,8 @@ const defaultQueryParams: OnlineQueryParams = {
userName: undefined,
};
+const inflightOnlineRequests = new Map>>>();
+
const parseTime = (time?: string | number | Date, pattern = 'YYYY-MM-DD HH:mm:ss'): string => {
return time ? dayjs(time).format(pattern) : '';
};
@@ -31,7 +33,24 @@ const normalizeDateValue = (value: unknown): string | number | Date | undefined
return undefined;
};
+const serializeOnlineQuery = (query: OnlineQueryParams) => JSON.stringify(query);
+
+const fetchOnlineList = async (query: OnlineQueryParams) => {
+ const requestKey = serializeOnlineQuery(query);
+ const existingRequest = inflightOnlineRequests.get(requestKey);
+ if (existingRequest) {
+ return existingRequest;
+ }
+
+ const requestPromise = listOnline(query).finally(() => {
+ inflightOnlineRequests.delete(requestKey);
+ });
+ inflightOnlineRequests.set(requestKey, requestPromise);
+ return requestPromise;
+};
+
const OnlineUserPage = () => {
+ const { message, modal } = App.useApp();
const { hasPermi } = usePermission();
const [queryForm] = Form.useForm();
const [loading, setLoading] = useState(false);
@@ -44,7 +63,7 @@ const OnlineUserPage = () => {
const getList = useCallback(async () => {
setLoading(true);
try {
- const response = await listOnline(queryParams);
+ const response = await fetchOnlineList(queryParams);
setList(response.rows ?? []);
setTotal(Number(response.total ?? 0));
} catch (error: unknown) {
@@ -53,7 +72,7 @@ const OnlineUserPage = () => {
} finally {
setLoading(false);
}
- }, [queryParams]);
+ }, [message, queryParams]);
useEffect(() => {
void getList();
@@ -79,7 +98,7 @@ const OnlineUserPage = () => {
}
const tokenId = row.tokenId;
- Modal.confirm({
+ modal.confirm({
title: '确认强退',
content: `是否确认强退名称为"${row.userName ?? ''}"的用户?`,
onOk: async () => {
diff --git a/src/pages/monitor/OperationLogPage.tsx b/src/pages/monitor/OperationLogPage.tsx
index c14e8e4..8994541 100644
--- a/src/pages/monitor/OperationLogPage.tsx
+++ b/src/pages/monitor/OperationLogPage.tsx
@@ -1,13 +1,13 @@
-import { useCallback, useEffect, useState } from 'react';
+import { useCallback, useEffect, useMemo, useState } from 'react';
import type { Key } from 'react';
import {
+ App,
Table,
Form,
Input,
Select,
Button,
Modal,
- message,
Space,
Tag,
DatePicker,
@@ -64,6 +64,8 @@ const defaultQueryParams: OperlogQueryParams = {
isAsc: 'descending',
};
+const inflightOperlogRequests = new Map>>>();
+
const parseTime = (time?: string | number | Date, pattern = 'YYYY-MM-DD HH:mm:ss'): string => {
return time ? dayjs(time).format(pattern) : '';
};
@@ -83,7 +85,25 @@ const escapeCsvCell = (value: unknown): string => {
return `"${raw.replace(/"/g, '""')}"`;
};
+const serializeOperlogQuery = (query: OperlogQueryParams) => JSON.stringify(query);
+
+const fetchOperlogList = async (query: OperlogQueryParams) => {
+ const requestKey = serializeOperlogQuery(query);
+ const existingRequest = inflightOperlogRequests.get(requestKey);
+ if (existingRequest) {
+ return existingRequest;
+ }
+
+ const requestPromise = listOperlog(query).finally(() => {
+ inflightOperlogRequests.delete(requestKey);
+ });
+
+ inflightOperlogRequests.set(requestKey, requestPromise);
+ return requestPromise;
+};
+
const OperationLogPage = () => {
+ const { message, modal } = App.useApp();
const [queryForm] = Form.useForm();
const [loading, setLoading] = useState(false);
const [list, setList] = useState([]);
@@ -94,19 +114,22 @@ const OperationLogPage = () => {
const [currentOperlogDetail, setCurrentOperlogDetail] = useState({});
const [queryParams, setQueryParams] = useState(defaultQueryParams);
+ const requestParams = useMemo(() => {
+ const formattedQueryParams: OperlogQueryParams = { ...queryParams };
+ if (dateRange?.[0] && dateRange?.[1]) {
+ formattedQueryParams.beginTime = dateRange[0].format('YYYY-MM-DD HH:mm:ss');
+ formattedQueryParams.endTime = dateRange[1].format('YYYY-MM-DD HH:mm:ss');
+ } else {
+ formattedQueryParams.beginTime = undefined;
+ formattedQueryParams.endTime = undefined;
+ }
+ return formattedQueryParams;
+ }, [dateRange, queryParams]);
+
const getList = useCallback(async () => {
setLoading(true);
try {
- const formattedQueryParams: OperlogQueryParams = { ...queryParams };
- if (dateRange?.[0] && dateRange?.[1]) {
- formattedQueryParams.beginTime = dateRange[0].format('YYYY-MM-DD HH:mm:ss');
- formattedQueryParams.endTime = dateRange[1].format('YYYY-MM-DD HH:mm:ss');
- } else {
- formattedQueryParams.beginTime = undefined;
- formattedQueryParams.endTime = undefined;
- }
-
- const response = await listOperlog(formattedQueryParams);
+ const response = await fetchOperlogList(requestParams);
setList(response.rows ?? []);
setTotal(Number(response.total ?? 0));
} catch (error: unknown) {
@@ -115,7 +138,7 @@ const OperationLogPage = () => {
} finally {
setLoading(false);
}
- }, [dateRange, queryParams]);
+ }, [message, requestParams]);
useEffect(() => {
void getList();
@@ -143,7 +166,7 @@ const OperationLogPage = () => {
return;
}
- Modal.confirm({
+ modal.confirm({
title: '确认删除',
content: `是否确认删除日志编号为"${operIds}"的数据项?`,
onOk: async () => {
@@ -160,7 +183,7 @@ const OperationLogPage = () => {
};
const handleClean = async () => {
- Modal.confirm({
+ modal.confirm({
title: '确认清空',
content: '是否确认清空所有操作日志数据项?',
onOk: async () => {
@@ -176,12 +199,12 @@ const OperationLogPage = () => {
});
};
- const operTypeLabel = (type?: string): string => {
+ const operTypeLabel = (type?: string | number): string => {
const dict = sysOperTypeDict.find((item) => item.value === String(type ?? ''));
return dict?.label ?? String(type ?? '');
};
- const operStatusLabel = (status?: string): string => {
+ const operStatusLabel = (status?: string | number): string => {
const dict = sysCommonStatusDict.find((item) => item.value === String(status ?? ''));
return dict?.label ?? String(status ?? '');
};
@@ -189,8 +212,8 @@ const OperationLogPage = () => {
const handleExport = async () => {
const hide = message.loading('正在导出数据...', 0);
try {
- const response = await listOperlog({
- ...queryParams,
+ const response = await fetchOperlogList({
+ ...requestParams,
pageNum: undefined,
pageSize: undefined,
});
@@ -242,12 +265,12 @@ const OperationLogPage = () => {
setDetailModalVisible(true);
};
- const operTypeFormat = (type?: string) => {
+ const operTypeFormat = (type?: string | number) => {
const dict = sysOperTypeDict.find((item) => item.value === String(type ?? ''));
return dict ? {dict.label} : String(type ?? '');
};
- const operStatusFormat = (status?: string) => {
+ const operStatusFormat = (status?: string | number) => {
const dict = sysCommonStatusDict.find((item) => item.value === String(status ?? ''));
return dict ? {dict.label} : String(status ?? '');
};
@@ -277,7 +300,7 @@ const OperationLogPage = () => {
title: '操作类型',
dataIndex: 'businessType',
align: 'center',
- render: (text) => operTypeFormat(typeof text === 'string' ? text : undefined),
+ render: (text) => operTypeFormat(typeof text === 'string' || typeof text === 'number' ? text : undefined),
},
{ title: '操作人员', dataIndex: 'operName', align: 'center', ellipsis: true, sorter: true },
{ title: '操作地址', dataIndex: 'operIp', align: 'center', ellipsis: true },
@@ -286,7 +309,7 @@ const OperationLogPage = () => {
title: '操作状态',
dataIndex: 'status',
align: 'center',
- render: (text) => operStatusFormat(typeof text === 'string' ? text : undefined),
+ render: (text) => operStatusFormat(typeof text === 'string' || typeof text === 'number' ? text : undefined),
},
{
title: '操作日期',
@@ -431,7 +454,9 @@ const OperationLogPage = () => {
{currentOperlogDetail.title} /{' '}
{operTypeFormat(
- typeof currentOperlogDetail.businessType === 'string' ? currentOperlogDetail.businessType : undefined,
+ typeof currentOperlogDetail.businessType === 'string' || typeof currentOperlogDetail.businessType === 'number'
+ ? currentOperlogDetail.businessType
+ : undefined,
)}
@@ -449,7 +474,11 @@ const OperationLogPage = () => {
{currentOperlogDetail.jsonResult}
- {operStatusFormat(typeof currentOperlogDetail.status === 'string' ? currentOperlogDetail.status : undefined)}
+ {operStatusFormat(
+ typeof currentOperlogDetail.status === 'string' || typeof currentOperlogDetail.status === 'number'
+ ? currentOperlogDetail.status
+ : undefined,
+ )}
{typeof currentOperlogDetail.costTime === 'number' ? `${currentOperlogDetail.costTime}毫秒` : ''}
diff --git a/src/pages/monitor/ServerMonitorPage.tsx b/src/pages/monitor/ServerMonitorPage.tsx
index 67c966e..0416940 100644
--- a/src/pages/monitor/ServerMonitorPage.tsx
+++ b/src/pages/monitor/ServerMonitorPage.tsx
@@ -23,6 +23,8 @@ const defaultServerInfo: ServerInfoResponse = {
sysFiles: [],
};
+let inflightServerInfoRequest: Promise | null = null;
+
const parseTime = (time?: string | number | Date, pattern = 'YYYY-MM-DD HH:mm:ss'): string => {
return time ? dayjs(time).format(pattern) : '';
};
@@ -65,7 +67,12 @@ const ServerMonitorPage = () => {
setLoading(true);
const hide = message.loading('正在加载服务监控数据,请稍候!', 0);
try {
- const response = await getServerInfo();
+ if (!inflightServerInfoRequest) {
+ inflightServerInfoRequest = getServerInfo().finally(() => {
+ inflightServerInfoRequest = null;
+ });
+ }
+ const response = await inflightServerInfoRequest;
setServerInfo(response);
} catch (error: unknown) {
console.error('Failed to fetch server info:', error);
@@ -77,7 +84,7 @@ const ServerMonitorPage = () => {
};
void getList();
- }, []);
+ }, [message]);
const diskColumns: TableColumnsType = [
{ title: '盘符路径', dataIndex: 'dirName', align: 'center' },
diff --git a/src/pages/project/DemandManagePage.tsx b/src/pages/project/DemandManagePage.tsx
index 9431086..b0312e8 100644
--- a/src/pages/project/DemandManagePage.tsx
+++ b/src/pages/project/DemandManagePage.tsx
@@ -1,5 +1,6 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import {
+ App,
Badge,
Button,
Card,
@@ -9,7 +10,6 @@ import {
Form,
Input,
InputNumber,
- message,
Modal,
Pagination,
Popconfirm,
@@ -265,6 +265,7 @@ const buildEndDate = (start: Dayjs, workHours: number) => {
};
const DemandManagePage: React.FC = () => {
+ const { message, modal } = App.useApp();
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const [queryForm] = Form.useForm();
@@ -554,8 +555,7 @@ const DemandManagePage: React.FC = () => {
}, [pageNum, pageSize, projectId, query, selectedVersion, toDemandRow]);
useEffect(() => {
- loadDictOptions();
- loadUserOptions();
+ void Promise.all([loadDictOptions(), loadUserOptions()]);
}, [loadDictOptions, loadUserOptions]);
useEffect(() => {
@@ -747,8 +747,7 @@ const DemandManagePage: React.FC = () => {
setDemandModalOpen(false);
setSelectedRowKeys([]);
- fetchDemands();
- fetchVersions();
+ void Promise.all([fetchDemands(), fetchVersions()]);
} catch {
// Validate/API error is handled by antd/request interceptor.
} finally {
@@ -790,7 +789,7 @@ const DemandManagePage: React.FC = () => {
);
} catch {
message.error('更新失败');
- fetchDemands();
+ void fetchDemands();
}
};
@@ -799,8 +798,7 @@ const DemandManagePage: React.FC = () => {
await deleteDemand(record.id);
message.success('删除成功');
setSelectedRowKeys([]);
- fetchDemands();
- fetchVersions();
+ void Promise.all([fetchDemands(), fetchVersions()]);
} catch {
message.error('删除失败');
}
@@ -815,8 +813,7 @@ const DemandManagePage: React.FC = () => {
await deleteDemandBatch(selectedRowKeys.join(','));
message.success('删除成功');
setSelectedRowKeys([]);
- fetchDemands();
- fetchVersions();
+ void Promise.all([fetchDemands(), fetchVersions()]);
} catch {
message.error('删除失败');
}
@@ -852,8 +849,7 @@ const DemandManagePage: React.FC = () => {
}
setVersionModalOpen(false);
- fetchVersions();
- fetchDemands();
+ void Promise.all([fetchVersions(), fetchDemands()]);
} catch {
// Validate/API error handled by antd/request interceptor.
} finally {
@@ -868,8 +864,7 @@ const DemandManagePage: React.FC = () => {
if (selectedVersion.nodeId === item.nodeId) {
setSelectedVersion({ id: projectId, nodeId: 'all', type: 2, title: '全部版本' });
}
- fetchVersions();
- fetchDemands();
+ void Promise.all([fetchVersions(), fetchDemands()]);
} catch {
message.error('版本删除失败');
}
@@ -885,7 +880,7 @@ const DemandManagePage: React.FC = () => {
return;
}
if (action === 'delete') {
- Modal.confirm({
+ modal.confirm({
title: '此操作将永久删除该版本号,是否继续?',
okText: '确定',
cancelText: '取消',
@@ -1292,7 +1287,7 @@ const DemandManagePage: React.FC = () => {
title="需求列表"
extra={
} onClick={() => navigate('/project/list')}>
- 返回项目列表
+ 返回
}
>
diff --git a/src/pages/project/ProjectDetailPage.tsx b/src/pages/project/ProjectDetailPage.tsx
index 5926db2..550de5f 100644
--- a/src/pages/project/ProjectDetailPage.tsx
+++ b/src/pages/project/ProjectDetailPage.tsx
@@ -1,11 +1,12 @@
import React, { useState, useEffect } from 'react';
-import { Form, Input, Button, message, Card, Row, Col, DatePicker, InputNumber } from 'antd';
+import { Form, Input, Button, Card, Row, Col, DatePicker, InputNumber } from 'antd';
import { useParams, useNavigate, useSearchParams } from 'react-router-dom';
import { getProjectDetail, addProject, updateProject, getProjectCode } from '../../api/project';
import dayjs from 'dayjs';
import { UserOutlined } from '@ant-design/icons';
import PageBackButton from '@/components/PageBackButton';
import { usePermission } from '@/contexts/PermissionContext';
+import { notify } from '@/utils/notify';
import './project-detail.css';
const { TextArea } = Input;
@@ -37,7 +38,7 @@ const ProjectDetailPage: React.FC = () => {
endDate: endValue ? dayjs(endValue) : null,
});
} catch (error) {
- message.error('获取项目详情失败');
+ notify.error('获取项目详情失败');
} finally {
setLoading(false);
}
@@ -47,7 +48,7 @@ const ProjectDetailPage: React.FC = () => {
const response = await getProjectCode();
form.setFieldsValue({ projectCode: ((response as Record).data ?? response) as string });
} catch(error) {
- message.error('获取项目编号失败');
+ notify.error('获取项目编号失败');
}
}
};
@@ -67,14 +68,14 @@ const ProjectDetailPage: React.FC = () => {
};
if (isEdit) {
await updateProject({ ...payload, projectId: id });
- message.success('修改成功');
+ notify.success('修改成功');
} else {
await addProject(payload);
- message.success('新增成功');
+ notify.success('新增成功');
}
navigate('/project/list');
} catch (error) {
- message.error(isEdit ? '修改失败' : '新增失败');
+ notify.error(isEdit ? '修改失败' : '新增失败');
} finally {
setLoading(false);
}
diff --git a/src/pages/projectBank/ProjectUserPage.tsx b/src/pages/projectBank/ProjectUserPage.tsx
index 2db9dfc..d0ddf81 100644
--- a/src/pages/projectBank/ProjectUserPage.tsx
+++ b/src/pages/projectBank/ProjectUserPage.tsx
@@ -1,5 +1,5 @@
-import { useEffect, useMemo, useState } from 'react';
-import { Button, DatePicker, Empty, Select, Spin, Table, message } from 'antd';
+import { useEffect, useMemo, useRef, useState } from 'react';
+import { Button, DatePicker, Empty, Select, Spin, Table } from 'antd';
import type { TableColumnsType } from 'antd';
import { CalendarOutlined } from '@ant-design/icons';
import { useNavigate, useSearchParams } from 'react-router-dom';
@@ -8,10 +8,11 @@ import type { Dayjs } from 'dayjs';
import zhCN from 'antd/es/date-picker/locale/zh_CN';
import PageBackButton from '@/components/PageBackButton';
import { getProjectDetail, listProject } from '@/api/project';
-import { getProjectExecutionInfo, getProjectWorkInfo } from '@/api/projectExecution';
+import { getProjectWorkInfo } from '@/api/projectExecution';
import { usePermission } from '@/contexts/PermissionContext';
import '@/styles/permission-link.css';
import { parseTime } from '@/utils/ruoyi';
+import { notify } from '@/utils/notify';
import './project-user.css';
const { RangePicker } = DatePicker;
@@ -43,6 +44,12 @@ interface ProjectOption {
projectName: string;
}
+type ProjectWorkInfoRequestParams = {
+ startDate: string;
+ endDate: string;
+ projectId: string | number;
+};
+
const isObject = (value: unknown): value is Record =>
typeof value === 'object' && value !== null;
@@ -54,22 +61,6 @@ const toNumber = (value: unknown, fallback = 0) => {
return Number.isFinite(num) ? num : fallback;
};
-const extractProjectList = (payload: unknown): ProjectOption[] => {
- const data = normalizeResponseData(payload);
- const list = Array.isArray(data)
- ? data
- : isObject(data) && Array.isArray(data.rows)
- ? data.rows
- : [];
- return list
- .filter(isObject)
- .map((item) => ({
- projectId: item.projectId as string | number,
- projectName: String(item.projectName ?? ''),
- }))
- .filter((item) => item.projectId !== undefined && item.projectId !== null && item.projectName);
-};
-
const extractProjectRows = (payload: unknown): Record[] => {
const data = normalizeResponseData(payload);
if (Array.isArray(data)) {
@@ -115,11 +106,58 @@ const normalizeDateParam = (value: string | null) => {
return parsed.isValid() ? parsed.format('YYYY-MM-DD') : null;
};
+let inflightProjectListRequest: Promise | null = null;
+const inflightProjectDetailRequests = new Map>();
+const inflightProjectWorkInfoRequests = new Map>();
+
+const fetchProjectListRequest = async () => {
+ if (inflightProjectListRequest) {
+ return inflightProjectListRequest;
+ }
+
+ const requestPromise = listProject({ pageNum: 1, pageSize: 1000 }).finally(() => {
+ inflightProjectListRequest = null;
+ });
+ inflightProjectListRequest = requestPromise;
+ return requestPromise;
+};
+
+const fetchProjectDetailRequest = async (projectId: string | number) => {
+ const requestKey = String(projectId);
+ const existingRequest = inflightProjectDetailRequests.get(requestKey);
+ if (existingRequest) {
+ return existingRequest;
+ }
+
+ const requestPromise = getProjectDetail(projectId).finally(() => {
+ inflightProjectDetailRequests.delete(requestKey);
+ });
+ inflightProjectDetailRequests.set(requestKey, requestPromise);
+ return requestPromise;
+};
+
+const serializeProjectWorkInfoRequest = (params: ProjectWorkInfoRequestParams) => JSON.stringify(params);
+
+const fetchProjectWorkInfoRequest = async (params: ProjectWorkInfoRequestParams) => {
+ const requestKey = serializeProjectWorkInfoRequest(params);
+ const existingRequest = inflightProjectWorkInfoRequests.get(requestKey);
+ if (existingRequest) {
+ return existingRequest;
+ }
+
+ const requestPromise = getProjectWorkInfo(params).finally(() => {
+ inflightProjectWorkInfoRequests.delete(requestKey);
+ });
+ inflightProjectWorkInfoRequests.set(requestKey, requestPromise);
+ return requestPromise;
+};
+
const ProjectUserPage = () => {
const navigate = useNavigate();
const { canAccessPath } = usePermission();
const [searchParams] = useSearchParams();
const projectId = searchParams.get('projectId') ?? searchParams.get('id') ?? '';
+ const queryProjectName = searchParams.get('projectName') ?? '';
const queryStartDate = normalizeDateParam(searchParams.get('startDate'));
const queryEndDate = normalizeDateParam(searchParams.get('endDate'));
const defaultStartDate = queryStartDate ?? dayjs().startOf('month').format('YYYY-MM-DD');
@@ -135,6 +173,10 @@ const ProjectUserPage = () => {
dayjs(defaultEndDate, 'YYYY-MM-DD'),
]);
const [detailList, setDetailList] = useState([]);
+ const [projectMetaReadyKey, setProjectMetaReadyKey] = useState('');
+ const projectDetailPendingRef = useRef('');
+ const projectListRequestIdRef = useRef(0);
+ const projectWorkInfoRequestIdRef = useRef(0);
const canViewWorkLog = canAccessPath('/index');
const openUserLog = (row: ProjectUserItem, queryDate?: string) => {
@@ -164,30 +206,26 @@ const ProjectUserPage = () => {
useEffect(() => {
const fetchProjectList = async () => {
+ const requestId = ++projectListRequestIdRef.current;
setLoading(true);
try {
- let nextProjectList: ProjectOption[] = [];
-
- try {
- const response = await getProjectExecutionInfo({
- startDate: `${dateRange[0].format('YYYY-MM-DD')} 00:00:00`,
- endDate: `${dateRange[1].format('YYYY-MM-DD')} 23:59:59`,
- });
- nextProjectList = extractProjectList(response);
- } catch (error) {
- console.warn('Project execution info timeout, fallback to project list:', error);
- const response = await listProject({ pageNum: 1, pageSize: 1000 });
- nextProjectList = extractProjectRows(response).map((item) => ({
+ const response = await fetchProjectListRequest();
+ if (requestId !== projectListRequestIdRef.current) {
+ return;
+ }
+ const nextProjectList = extractProjectRows(response)
+ .map((item) => ({
projectId: item.projectId as string | number,
projectName: String(item.projectName ?? ''),
- })).filter((item) => item.projectId !== undefined && item.projectId !== null && item.projectName);
- }
+ }))
+ .filter((item) => item.projectId !== undefined && item.projectId !== null && item.projectName);
setProjectList(nextProjectList);
if (nextProjectList.length === 0) {
- setSelectedProjectId(undefined);
+ setSelectedProjectId(projectId || undefined);
setProjectDetail({});
setDetailList([]);
+ setProjectMetaReadyKey('');
return;
}
setSelectedProjectId((prev) => {
@@ -203,10 +241,16 @@ const ProjectUserPage = () => {
return nextProjectList[0].projectId;
});
} catch (error) {
+ if (requestId !== projectListRequestIdRef.current) {
+ return;
+ }
console.error('Failed to fetch project list:', error);
- message.error('获取项目列表失败');
+ setProjectList([]);
+ notify.error('获取项目列表失败');
} finally {
- setLoading(false);
+ if (requestId === projectListRequestIdRef.current) {
+ setLoading(false);
+ }
}
};
void fetchProjectList();
@@ -215,11 +259,18 @@ const ProjectUserPage = () => {
useEffect(() => {
const fetchProjectData = async () => {
if (!selectedProjectId) {
+ setProjectMetaReadyKey('');
return;
}
+ const activeProjectId = String(selectedProjectId);
+ projectDetailPendingRef.current = activeProjectId;
+ setProjectMetaReadyKey('');
setLoading(true);
try {
- const detailResponse = await getProjectDetail(selectedProjectId);
+ const detailResponse = await fetchProjectDetailRequest(selectedProjectId);
+ if (projectDetailPendingRef.current !== activeProjectId) {
+ return;
+ }
const detail = normalizeProjectDetail(detailResponse);
setProjectDetail(detail);
@@ -246,9 +297,14 @@ const ProjectUserPage = () => {
if (useRoutePreset) {
setRoutePresetApplied(true);
}
+ projectDetailPendingRef.current = '';
+ setProjectMetaReadyKey(`${activeProjectId}:${nextStart.valueOf()}:${nextEnd.valueOf()}`);
} catch (error) {
+ if (projectDetailPendingRef.current === activeProjectId) {
+ projectDetailPendingRef.current = '';
+ }
console.error('Failed to fetch project detail:', error);
- message.error('获取项目详情失败');
+ notify.error('获取项目详情失败');
} finally {
setLoading(false);
}
@@ -258,27 +314,40 @@ const ProjectUserPage = () => {
useEffect(() => {
const fetchWorkInfo = async () => {
- if (!selectedProjectId) {
+ if (!selectedProjectId || !projectMetaReadyKey) {
return;
}
+ if (projectDetailPendingRef.current === String(selectedProjectId)) {
+ return;
+ }
+ const requestId = ++projectWorkInfoRequestIdRef.current;
+ const requestParams = {
+ startDate: `${dateRange[0].format('YYYY-MM-DD')} 00:00:00`,
+ endDate: `${dateRange[1].format('YYYY-MM-DD')} 23:59:59`,
+ projectId: selectedProjectId,
+ };
setLoading(true);
try {
- const response = await getProjectWorkInfo({
- startDate: `${dateRange[0].format('YYYY-MM-DD')} 00:00:00`,
- endDate: `${dateRange[1].format('YYYY-MM-DD')} 23:59:59`,
- projectId: selectedProjectId,
- });
+ const response = await fetchProjectWorkInfoRequest(requestParams);
+ if (requestId !== projectWorkInfoRequestIdRef.current) {
+ return;
+ }
setDetailList(extractDetailList(response));
} catch (error) {
+ if (requestId !== projectWorkInfoRequestIdRef.current) {
+ return;
+ }
console.error('Failed to fetch project work info:', error);
- message.error('获取项目人员表失败');
+ notify.error('获取项目人员表失败');
setDetailList([]);
} finally {
- setLoading(false);
+ if (requestId === projectWorkInfoRequestIdRef.current) {
+ setLoading(false);
+ }
}
};
void fetchWorkInfo();
- }, [dateRange, selectedProjectId]);
+ }, [dateRange, projectMetaReadyKey, selectedProjectId]);
const columns = useMemo>>(() => {
return dateRange[0]
@@ -340,6 +409,34 @@ const ProjectUserPage = () => {
return hit?.projectName ?? '';
}, [projectList, selectedProjectId]);
+ const selectedProjectLabel = useMemo(() => {
+ return projectDetail.projectName || selectedProjectName || queryProjectName || String(selectedProjectId ?? '');
+ }, [projectDetail.projectName, queryProjectName, selectedProjectId, selectedProjectName]);
+
+ const projectSelectOptions = useMemo(() => {
+ const baseOptions = projectList.map((item) => ({
+ label: item.projectName,
+ value: item.projectId,
+ }));
+
+ if (selectedProjectId === undefined || selectedProjectId === null || selectedProjectId === '') {
+ return baseOptions;
+ }
+
+ const exists = baseOptions.some((item) => String(item.value) === String(selectedProjectId));
+ if (exists) {
+ return baseOptions;
+ }
+
+ return [
+ {
+ label: selectedProjectLabel,
+ value: selectedProjectId,
+ },
+ ...baseOptions,
+ ];
+ }, [projectList, selectedProjectId, selectedProjectLabel]);
+
return (
@@ -352,12 +449,9 @@ const ProjectUserPage = () => {
选择项目
-
+
{monthDayList.map((item) => {
const className = [
'worklog-strip-day',
@@ -1244,6 +1405,7 @@ const WorkLogPage = () => {
key={item.key}
type="button"
className={className}
+ ref={item.isSelected ? selectedDayButtonRef : null}
onClick={() => handlePickDay(item.current)}
disabled={item.isFuture}
>
diff --git a/vite.config.ts b/vite.config.ts
index 7226079..b2ed1ef 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -5,7 +5,12 @@ import path from 'path' // Import path module
const inferChunkName = (moduleIds: string[]) => {
const joined = moduleIds.join('\n')
- if (joined.includes('/node_modules/antd/es/table') || joined.includes('/node_modules/rc-table/')) {
+ if (
+ joined.includes('/node_modules/rc-pagination/') ||
+ joined.includes('/node_modules/rc-table/') ||
+ joined.includes('/node_modules/rc-resize-observer/') ||
+ joined.includes('/node_modules/rc-virtual-list/')
+ ) {
return 'antd-table'
}
if (joined.includes('/node_modules/antd/es/input') || joined.includes('/node_modules/rc-input/')) {
@@ -26,7 +31,7 @@ const inferChunkName = (moduleIds: string[]) => {
if (joined.includes('/node_modules/antd/es/locale') || joined.includes('/node_modules/antd/locale/')) {
return 'antd-locale'
}
- if (joined.includes('/node_modules/@ant-design/icons/')) {
+ if (joined.includes('/node_modules/@ant-design/icons/') || joined.includes('/node_modules/@ant-design/icons-svg/')) {
return 'antd-icons'
}
if (joined.includes('/src/components/')) {
@@ -63,7 +68,19 @@ export default defineConfig({
if (/node_modules\/(react|react-dom|scheduler)\//.test(id)) {
return 'react-vendor';
}
- if (/node_modules\/(echarts|echarts-for-react)\//.test(id)) {
+ if (/node_modules\/(@ant-design\/icons|@ant-design\/icons-svg)\//.test(id)) {
+ return 'antd-icons';
+ }
+ if (/node_modules\/(rc-table|rc-pagination|rc-resize-observer|rc-virtual-list)\//.test(id)) {
+ return 'antd-table';
+ }
+ if (/node_modules\/echarts-for-react\//.test(id)) {
+ return 'echarts-react';
+ }
+ if (/node_modules\/zrender\//.test(id)) {
+ return 'zrender-vendor';
+ }
+ if (/node_modules\/echarts\//.test(id)) {
return 'echarts-vendor';
}
if (/node_modules\/(react-router|react-router-dom)\//.test(id)) {