Skip to content

Commit 17960f6

Browse files
committed
feat: enhance useFetchData hook with polling functionality and improve task progress tracking
1 parent 69b9517 commit 17960f6

File tree

9 files changed

+258
-178
lines changed

9 files changed

+258
-178
lines changed

frontend/src/components/CardView.tsx

Lines changed: 66 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ interface CardViewProps<T> {
3737
| {
3838
key: string;
3939
label: string;
40+
danger?: boolean;
4041
icon?: React.JSX.Element;
4142
onClick?: (item: T) => void;
4243
}[]
@@ -169,82 +170,85 @@ function CardView<T extends BaseCardDataType>(props: CardViewProps<T>) {
169170
typeof operations === "function" ? operations(item) : operations;
170171
return (
171172
<div className="flex-overflow-hidden">
172-
<div className="flex-overflow-auto grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4 gap-4">
173+
<div className="overflow-auto grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4 gap-4">
173174
{data.map((item) => (
174175
<div
175176
key={item.id}
176177
className="border-card p-4 bg-white hover:shadow-lg transition-shadow duration-200"
177178
>
178179
<div className="flex flex-col space-y-4 h-full">
179-
{/* Header */}
180-
<div className="flex items-start justify-between">
181-
<div className="flex items-center gap-3 min-w-0">
182-
{item?.icon && (
183-
<div
184-
className={`flex-shrink-0 w-12 h-12 ${
185-
item?.iconColor ||
186-
"bg-gradient-to-br from-blue-100 to-blue-200"
187-
} rounded-lg flex items-center justify-center`}
188-
>
189-
{item?.icon}
190-
</div>
191-
)}
192-
<div className="flex-1 min-w-0">
193-
<div className="flex items-center gap-2 mb-1">
194-
<h3
195-
className={`text-base flex-1 text-ellipsis overflow-hidden whitespace-nowrap font-semibold text-gray-900 truncate ${
196-
onView ? "cursor-pointer hover:text-blue-600" : ""
197-
}`}
198-
onClick={() => onView?.(item)}
180+
<div
181+
className="flex flex-col space-y-4 h-full"
182+
onClick={() => onView?.(item)}
183+
style={{ cursor: onView ? "pointer" : "default" }}
184+
>
185+
{/* Header */}
186+
<div className="flex items-start justify-between">
187+
<div className="flex items-center gap-3 min-w-0">
188+
{item?.icon && (
189+
<div
190+
className={`flex-shrink-0 w-12 h-12 ${
191+
item?.iconColor ||
192+
"bg-gradient-to-br from-blue-100 to-blue-200"
193+
} rounded-lg flex items-center justify-center`}
199194
>
200-
{item?.name}
201-
</h3>
202-
{item?.status && (
203-
<Tag color={item?.status?.color}>
204-
<div className="flex items-center gap-2 text-xs py-0.5">
205-
<span>{item?.status?.icon}</span>
206-
<span>{item?.status?.label}</span>
207-
</div>
208-
</Tag>
209-
)}
195+
{item?.icon}
196+
</div>
197+
)}
198+
<div className="flex-1 min-w-0">
199+
<div className="flex items-center gap-2 mb-1">
200+
<h3
201+
className={`text-base flex-1 text-ellipsis overflow-hidden whitespace-nowrap font-semibold text-gray-900 truncate`}
202+
>
203+
{item?.name}
204+
</h3>
205+
{item?.status && (
206+
<Tag color={item?.status?.color}>
207+
<div className="flex items-center gap-2 text-xs py-0.5">
208+
<span>{item?.status?.icon}</span>
209+
<span>{item?.status?.label}</span>
210+
</div>
211+
</Tag>
212+
)}
213+
</div>
210214
</div>
211215
</div>
216+
{onFavorite && (
217+
<StarFilled
218+
style={{
219+
fontSize: "16px",
220+
color: isFavorite?.(item) ? "#ffcc00ff" : "#d1d5db",
221+
cursor: "pointer",
222+
}}
223+
onClick={() => onFavorite?.(item)}
224+
/>
225+
)}
212226
</div>
213-
{onFavorite && (
214-
<StarFilled
215-
style={{
216-
fontSize: "16px",
217-
color: isFavorite?.(item) ? "#ffcc00ff" : "#d1d5db",
218-
cursor: "pointer",
219-
}}
220-
onClick={() => onFavorite?.(item)}
221-
/>
222-
)}
223-
</div>
224227

225-
<div className="flex-1 flex flex-col justify-end">
226-
{/* Tags */}
227-
<TagsRenderer tags={item?.tags || []} />
228+
<div className="flex-1 flex flex-col justify-end">
229+
{/* Tags */}
230+
<TagsRenderer tags={item?.tags || []} />
228231

229-
{/* Description */}
230-
<p className="text-gray-600 text-xs text-ellipsis overflow-hidden whitespace-nowrap text-xs line-clamp-2 mt-2">
231-
<Tooltip title={item?.description}>
232-
{item?.description}
233-
</Tooltip>
234-
</p>
232+
{/* Description */}
233+
<p className="text-gray-600 text-xs text-ellipsis overflow-hidden whitespace-nowrap text-xs line-clamp-2 mt-2">
234+
<Tooltip title={item?.description}>
235+
{item?.description}
236+
</Tooltip>
237+
</p>
235238

236-
{/* Statistics */}
237-
<div className="grid grid-cols-2 gap-4 py-3">
238-
{item?.statistics?.map((stat, idx) => (
239-
<div key={idx}>
240-
<div className="text-sm text-gray-500">
241-
{stat?.label}:
242-
</div>
243-
<div className="text-base font-semibold text-gray-900">
244-
{stat?.value}
239+
{/* Statistics */}
240+
<div className="grid grid-cols-2 gap-4 py-3">
241+
{item?.statistics?.map((stat, idx) => (
242+
<div key={idx}>
243+
<div className="text-sm text-gray-500">
244+
{stat?.label}:
245+
</div>
246+
<div className="text-base font-semibold text-gray-900">
247+
{stat?.value}
248+
</div>
245249
</div>
246-
</div>
247-
))}
250+
))}
251+
</div>
248252
</div>
249253
</div>
250254

frontend/src/hooks/useFetchData.ts

Lines changed: 133 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,32 @@
11
// 首页数据获取
2-
import { useState } from "react";
2+
// 支持轮询功能,使用示例:
3+
// const { fetchData, startPolling, stopPolling, isPolling } = useFetchData(
4+
// fetchFunction,
5+
// mapFunction,
6+
// 5000 // 5秒轮询一次,默认30秒
7+
// false // 是否自动开始轮询,默认 true
8+
// );
9+
//
10+
// startPolling(); // 开始轮询
11+
// stopPolling(); // 停止轮询
12+
// 手动调用 fetchData() 时,如果正在轮询,会重新开始轮询计时
13+
import { useState, useRef, useEffect, useCallback } from "react";
314
import { useDebouncedEffect } from "./useDebouncedEffect";
415
import Loading from "@/utils/loading";
516
import { App } from "antd";
617

718
export default function useFetchData<T>(
819
fetchFunc: (params?: any) => Promise<any>,
9-
mapDataFunc: (data: any) => T = (data) => data as T
20+
mapDataFunc: (data: any) => T = (data) => data as T,
21+
pollingInterval: number = 30000, // 默认30秒轮询一次
22+
autoRefresh: boolean = true
1023
) {
1124
const { message } = App.useApp();
25+
26+
// 轮询相关状态
27+
const [isPolling, setIsPolling] = useState(false);
28+
const pollingTimerRef = useRef<NodeJS.Timeout | null>(null);
29+
1230
// 表格数据
1331
const [tableData, setTableData] = useState<T[]>([]);
1432
// 设置加载状态
@@ -55,39 +73,108 @@ export default function useFetchData<T>(
5573
return arr[0];
5674
}
5775

58-
async function fetchData(extraParams = {}) {
59-
const { keyword, filter, current, pageSize } = searchParams;
60-
Loading.show();
61-
setLoading(true);
62-
try {
63-
const { data } = await fetchFunc({
64-
...filter,
65-
...extraParams,
66-
keyword,
67-
type: getFirstOfArray(filter?.type) || undefined,
68-
status: getFirstOfArray(filter?.status) || undefined,
69-
tags: filter?.tags?.length ? filter.tags.join(",") : undefined,
70-
page: current - 1,
71-
size: pageSize,
72-
});
73-
setPagination((prev) => ({
74-
...prev,
75-
total: data?.totalElements || 0,
76-
}));
77-
let result = [];
78-
if (mapDataFunc) {
79-
result = data?.content.map(mapDataFunc) ?? [];
80-
}
81-
setTableData(result);
82-
} catch (error) {
83-
console.error(error)
84-
message.error("数据获取失败,请稍后重试");
85-
} finally {
86-
Loading.hide();
87-
setLoading(false);
76+
// 清除轮询定时器
77+
const clearPollingTimer = useCallback(() => {
78+
if (pollingTimerRef.current) {
79+
clearTimeout(pollingTimerRef.current);
80+
pollingTimerRef.current = null;
8881
}
89-
}
82+
}, []);
83+
84+
const fetchData = useCallback(
85+
async (extraParams = {}, skipPollingRestart = false) => {
86+
const { keyword, filter, current, pageSize } = searchParams;
87+
88+
if (!skipPollingRestart) {
89+
Loading.show();
90+
setLoading(true);
91+
}
92+
93+
// 如果正在轮询且不是轮询触发的调用,先停止当前轮询
94+
const wasPolling = isPolling && !skipPollingRestart;
95+
if (wasPolling) {
96+
clearPollingTimer();
97+
}
9098

99+
try {
100+
const { data } = await fetchFunc({
101+
...filter,
102+
...extraParams,
103+
keyword,
104+
type: getFirstOfArray(filter?.type) || undefined,
105+
status: getFirstOfArray(filter?.status) || undefined,
106+
tags: filter?.tags?.length ? filter.tags.join(",") : undefined,
107+
page: current - 1,
108+
size: pageSize,
109+
});
110+
setPagination((prev) => ({
111+
...prev,
112+
total: data?.totalElements || 0,
113+
}));
114+
let result = [];
115+
if (mapDataFunc) {
116+
result = data?.content.map(mapDataFunc) ?? [];
117+
}
118+
setTableData(result);
119+
120+
// 如果之前正在轮询且不是轮询触发的调用,重新开始轮询
121+
if (wasPolling) {
122+
const poll = () => {
123+
pollingTimerRef.current = setTimeout(() => {
124+
fetchData({}, true).then(() => {
125+
if (pollingTimerRef.current) {
126+
poll();
127+
}
128+
});
129+
}, pollingInterval);
130+
};
131+
poll();
132+
}
133+
} catch (error) {
134+
console.error(error);
135+
message.error("数据获取失败,请稍后重试");
136+
} finally {
137+
Loading.hide();
138+
setLoading(false);
139+
}
140+
},
141+
[
142+
searchParams,
143+
fetchFunc,
144+
mapDataFunc,
145+
isPolling,
146+
clearPollingTimer,
147+
pollingInterval,
148+
message,
149+
]
150+
);
151+
152+
// 开始轮询
153+
const startPolling = useCallback(() => {
154+
clearPollingTimer();
155+
setIsPolling(true);
156+
157+
const poll = () => {
158+
pollingTimerRef.current = setTimeout(() => {
159+
fetchData({}, true).then(() => {
160+
if (pollingTimerRef.current) {
161+
poll();
162+
}
163+
});
164+
}, pollingInterval);
165+
};
166+
167+
poll();
168+
}, [pollingInterval, clearPollingTimer, fetchData]);
169+
170+
// 停止轮询
171+
const stopPolling = useCallback(() => {
172+
clearPollingTimer();
173+
setIsPolling(false);
174+
}, [clearPollingTimer]);
175+
176+
// 搜索参数变化时,自动刷新数据
177+
// keyword 变化时,防抖500ms后刷新
91178
useDebouncedEffect(
92179
() => {
93180
fetchData();
@@ -96,6 +183,16 @@ export default function useFetchData<T>(
96183
searchParams?.keyword ? 500 : 0
97184
);
98185

186+
// 组件卸载时清理轮询
187+
useEffect(() => {
188+
if (autoRefresh) {
189+
startPolling();
190+
}
191+
return () => {
192+
clearPollingTimer();
193+
};
194+
}, [clearPollingTimer]);
195+
99196
return {
100197
loading,
101198
tableData,
@@ -109,5 +206,8 @@ export default function useFetchData<T>(
109206
setPagination,
110207
handleFiltersChange,
111208
fetchData,
209+
isPolling,
210+
startPolling,
211+
stopPolling,
112212
};
113213
}

0 commit comments

Comments
 (0)