Skip to content

Commit bb11683

Browse files
committed
feat: Enhance DatasetDetail component with delete functionality and improved download handling
feat: Add automatic data refresh and improved user feedback in DatasetManagementPage fix: Update dataset API to streamline download functionality and improve error handling
1 parent a6d4b51 commit bb11683

File tree

19 files changed

+397
-1007
lines changed

19 files changed

+397
-1007
lines changed
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import { Dropdown, Popconfirm, Button, Space } from "antd";
2+
import { EllipsisOutlined } from "@ant-design/icons";
3+
import { useState } from "react";
4+
5+
interface ActionItem {
6+
key: string;
7+
label: string;
8+
icon?: React.ReactNode;
9+
danger?: boolean;
10+
confirm?: {
11+
title: string;
12+
description?: string;
13+
okText?: string;
14+
cancelText?: string;
15+
};
16+
}
17+
18+
interface ActionDropdownProps {
19+
actions?: ActionItem[];
20+
onAction?: (key: string, action: ActionItem) => void;
21+
placement?:
22+
| "bottomRight"
23+
| "topLeft"
24+
| "topCenter"
25+
| "topRight"
26+
| "bottomLeft"
27+
| "bottomCenter"
28+
| "top"
29+
| "bottom";
30+
}
31+
32+
const ActionDropdown = ({
33+
actions = [],
34+
onAction,
35+
placement = "bottomRight",
36+
}: ActionDropdownProps) => {
37+
const [open, setOpen] = useState(false);
38+
const handleActionClick = (action: ActionItem) => {
39+
if (action.confirm) {
40+
// 如果有确认框,不立即执行,等待确认
41+
return;
42+
}
43+
// 执行操作
44+
onAction?.(action.key, action);
45+
// 如果没有确认框,则立即关闭 Dropdown
46+
setOpen(false);
47+
};
48+
49+
const dropdownContent = (
50+
<div className="bg-white p-2 rounded shadow-md">
51+
<Space direction="vertical" className="w-full">
52+
{actions.map((action) => {
53+
if (action.confirm) {
54+
return (
55+
<Popconfirm
56+
key={action.key}
57+
title={action.confirm.title}
58+
description={action.confirm.description}
59+
onConfirm={() => {
60+
onAction?.(action.key, action);
61+
setOpen(false);
62+
}}
63+
okText={action.confirm.okText || "确定"}
64+
cancelText={action.confirm.cancelText || "取消"}
65+
okType={action.danger ? "danger" : "primary"}
66+
styles={{ root: { zIndex: 9999 } }}
67+
>
68+
<Button
69+
type="text"
70+
size="small"
71+
className="w-full text-left"
72+
danger={action.danger}
73+
icon={action.icon}
74+
>
75+
{action.label}
76+
</Button>
77+
</Popconfirm>
78+
);
79+
}
80+
81+
return (
82+
<Button
83+
key={action.key}
84+
className="w-full"
85+
size="small"
86+
type="text"
87+
danger={action.danger}
88+
icon={action.icon}
89+
onClick={() => handleActionClick(action)}
90+
>
91+
{action.label}
92+
</Button>
93+
);
94+
})}
95+
</Space>
96+
</div>
97+
);
98+
99+
return (
100+
<Dropdown
101+
overlay={dropdownContent}
102+
trigger={["click"]}
103+
placement={placement}
104+
open={open}
105+
onOpenChange={setOpen}
106+
>
107+
<Button
108+
type="text"
109+
icon={<EllipsisOutlined style={{ fontSize: 24 }} />}
110+
/>
111+
</Dropdown>
112+
);
113+
};
114+
115+
export default ActionDropdown;

frontend/src/components/AddTagPopover.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ export default function AddTagPopover({
6464
open={showPopover}
6565
trigger="click"
6666
placement="bottom"
67+
onOpenChange={setShowPopover}
6768
content={
6869
<div className="space-y-4 w-[300px]">
6970
<h4 className="font-medium border-b pb-2 border-gray-100">

frontend/src/components/CardView.tsx

Lines changed: 60 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
import React, { useState, useEffect, useRef } from "react";
2-
import { Tag, Pagination, Dropdown, Tooltip, Empty, Popover } from "antd";
32
import {
4-
EllipsisOutlined,
5-
ClockCircleOutlined,
6-
StarFilled,
7-
} from "@ant-design/icons";
3+
Tag,
4+
Pagination,
5+
Tooltip,
6+
Empty,
7+
Popover,
8+
Menu,
9+
Popconfirm,
10+
} from "antd";
11+
import { ClockCircleOutlined, StarFilled } from "@ant-design/icons";
812
import type { ItemType } from "antd/es/menu/interface";
913
import { formatDateTime } from "@/utils/unit";
14+
import ActionDropdown from "./ActionDropdown";
1015

1116
interface BaseCardDataType {
1217
id: string | number;
@@ -168,6 +173,48 @@ function CardView<T extends BaseCardDataType>(props: CardViewProps<T>) {
168173

169174
const ops = (item) =>
170175
typeof operations === "function" ? operations(item) : operations;
176+
177+
const menu = (item) => {
178+
const ops =
179+
typeof operations === "function" ? operations(item) : operations;
180+
<Menu>
181+
{ops.map((op) => {
182+
if (op?.danger) {
183+
return (
184+
<Menu.Item key={op?.key} disabled icon={op?.icon}>
185+
<Popconfirm
186+
title="确定删除吗?"
187+
description="此操作不可撤销"
188+
onConfirm={op.onClick ? () => op.onClick(item) : undefined}
189+
okText="确定"
190+
cancelText="取消"
191+
// 阻止事件冒泡,避免 Dropdown 关闭
192+
onClick={(e) => e.stopPropagation()}
193+
>
194+
<div
195+
style={{
196+
display: "block",
197+
width: "100%",
198+
color: "inherit",
199+
}}
200+
onClick={(e) => e.stopPropagation()}
201+
>
202+
{op.icon}
203+
{op.label}
204+
</div>
205+
</Popconfirm>
206+
</Menu.Item>
207+
);
208+
} else {
209+
return (
210+
<Menu.Item key={op?.key} onClick={op?.onClick} icon={op?.icon}>
211+
{op?.label}
212+
</Menu.Item>
213+
);
214+
}
215+
})}
216+
</Menu>;
217+
};
171218
return (
172219
<div className="flex-overflow-hidden">
173220
<div className="overflow-auto grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4 gap-4">
@@ -261,24 +308,15 @@ function CardView<T extends BaseCardDataType>(props: CardViewProps<T>) {
261308
</div>
262309
</div>
263310
{operations && (
264-
<Dropdown
265-
trigger={["click"]}
266-
menu={{
267-
items: ops(item),
268-
onClick: ({ key }) => {
269-
const operation = ops(item).find(
270-
(op) => op.key === key
271-
);
272-
if (operation?.onClick) {
273-
operation.onClick(item);
274-
}
275-
},
311+
<ActionDropdown
312+
actions={ops(item)}
313+
onAction={(key) => {
314+
const operation = ops(item).find((op) => op.key === key);
315+
if (operation?.onClick) {
316+
operation.onClick(item);
317+
}
276318
}}
277-
>
278-
<div className="cursor-pointer">
279-
<EllipsisOutlined style={{ fontSize: 24 }} />
280-
</div>
281-
</Dropdown>
319+
/>
282320
)}
283321
</div>
284322
</div>

frontend/src/components/DetailHeader.tsx

Lines changed: 31 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import React from "react";
22
import { Database } from "lucide-react";
3-
import { Card, Dropdown, Button, Tag, Tooltip } from "antd";
3+
import { Card, Button, Tag, Tooltip, Popconfirm } from "antd";
44
import type { ItemType } from "antd/es/menu/interface";
55
import AddTagPopover from "./AddTagPopover";
6+
import ActionDropdown from "./ActionDropdown";
67

78
interface StatisticItem {
89
icon: React.ReactNode;
@@ -100,22 +101,39 @@ function DetailHeader<T>({
100101
{operations.map((op) => {
101102
if (op.isDropdown) {
102103
return (
103-
<Dropdown
104-
key={op.key}
105-
menu={{
106-
items: op?.items as ItemType[],
107-
onClick: op?.onMenuClick,
108-
}}
109-
>
110-
<Tooltip title={op.label}>
111-
<Button icon={op.icon} />
112-
</Tooltip>
113-
</Dropdown>
104+
<ActionDropdown
105+
actions={op?.items}
106+
onAction={op?.onMenuClick}
107+
/>
108+
);
109+
}
110+
if (op.confirm) {
111+
return (
112+
<Tooltip key={op.key} title={op.label}>
113+
<Popconfirm
114+
key={op.key}
115+
title={op.confirm.title}
116+
description={op.confirm.description}
117+
onConfirm={() => {
118+
op?.onClick();
119+
}}
120+
okText={op.confirm.okText || "确定"}
121+
cancelText={op.confirm.cancelText || "取消"}
122+
okType={op.danger ? "danger" : "primary"}
123+
overlayStyle={{ zIndex: 9999 }}
124+
>
125+
<Button icon={op.icon} danger={op.danger} />
126+
</Popconfirm>
127+
</Tooltip>
114128
);
115129
}
116130
return (
117131
<Tooltip key={op.key} title={op.label}>
118-
<Button {...op} />
132+
<Button
133+
icon={op.icon}
134+
danger={op.danger}
135+
onClick={op.onClick}
136+
/>
119137
</Tooltip>
120138
);
121139
})}

frontend/src/hooks/useFetchData.ts

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,27 @@
33
// const { fetchData, startPolling, stopPolling, isPolling } = useFetchData(
44
// fetchFunction,
55
// mapFunction,
6-
// 5000 // 5秒轮询一次,默认30秒
7-
// false // 是否自动开始轮询,默认 true
6+
// 5000, // 5秒轮询一次,默认30秒
7+
// true, // 是否自动开始轮询,默认 true
8+
// [fetchStatistics, fetchOtherData] // 额外的轮询函数数组
89
// );
910
//
1011
// startPolling(); // 开始轮询
1112
// stopPolling(); // 停止轮询
1213
// 手动调用 fetchData() 时,如果正在轮询,会重新开始轮询计时
14+
// 轮询时会同时执行主要的 fetchFunction 和所有额外的轮询函数
1315
import { useState, useRef, useEffect, useCallback } from "react";
1416
import { useDebouncedEffect } from "./useDebouncedEffect";
1517
import Loading from "@/utils/loading";
1618
import { App } from "antd";
19+
import { AnyObject } from "antd/es/_util/type";
1720

1821
export default function useFetchData<T>(
1922
fetchFunc: (params?: any) => Promise<any>,
20-
mapDataFunc: (data: any) => T = (data) => data as T,
23+
mapDataFunc: (data: AnyObject) => T = (data) => data as T,
2124
pollingInterval: number = 30000, // 默认30秒轮询一次
22-
autoRefresh: boolean = true
25+
autoRefresh: boolean = true,
26+
additionalPollingFuncs: (() => Promise<any>)[] = [] // 额外的轮询函数
2327
) {
2428
const { message } = App.useApp();
2529

@@ -97,16 +101,24 @@ export default function useFetchData<T>(
97101
}
98102

99103
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-
});
104+
// 同时执行主要数据获取和额外的轮询函数
105+
const promises = [
106+
fetchFunc({
107+
...filter,
108+
...extraParams,
109+
keyword,
110+
type: getFirstOfArray(filter?.type) || undefined,
111+
status: getFirstOfArray(filter?.status) || undefined,
112+
tags: filter?.tags?.length ? filter.tags.join(",") : undefined,
113+
page: current - 1,
114+
size: pageSize,
115+
}),
116+
...additionalPollingFuncs.map((func) => func()),
117+
];
118+
119+
const results = await Promise.all(promises);
120+
const { data } = results[0]; // 主要数据结果
121+
110122
setPagination((prev) => ({
111123
...prev,
112124
total: data?.totalElements || 0,
@@ -146,6 +158,7 @@ export default function useFetchData<T>(
146158
clearPollingTimer,
147159
pollingInterval,
148160
message,
161+
additionalPollingFuncs,
149162
]
150163
);
151164

0 commit comments

Comments
 (0)