You've already forked DataMate
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
This commit is contained in:
115
frontend/src/components/ActionDropdown.tsx
Normal file
115
frontend/src/components/ActionDropdown.tsx
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
import { Dropdown, Popconfirm, Button, Space } from "antd";
|
||||||
|
import { EllipsisOutlined } from "@ant-design/icons";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
interface ActionItem {
|
||||||
|
key: string;
|
||||||
|
label: string;
|
||||||
|
icon?: React.ReactNode;
|
||||||
|
danger?: boolean;
|
||||||
|
confirm?: {
|
||||||
|
title: string;
|
||||||
|
description?: string;
|
||||||
|
okText?: string;
|
||||||
|
cancelText?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ActionDropdownProps {
|
||||||
|
actions?: ActionItem[];
|
||||||
|
onAction?: (key: string, action: ActionItem) => void;
|
||||||
|
placement?:
|
||||||
|
| "bottomRight"
|
||||||
|
| "topLeft"
|
||||||
|
| "topCenter"
|
||||||
|
| "topRight"
|
||||||
|
| "bottomLeft"
|
||||||
|
| "bottomCenter"
|
||||||
|
| "top"
|
||||||
|
| "bottom";
|
||||||
|
}
|
||||||
|
|
||||||
|
const ActionDropdown = ({
|
||||||
|
actions = [],
|
||||||
|
onAction,
|
||||||
|
placement = "bottomRight",
|
||||||
|
}: ActionDropdownProps) => {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const handleActionClick = (action: ActionItem) => {
|
||||||
|
if (action.confirm) {
|
||||||
|
// 如果有确认框,不立即执行,等待确认
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 执行操作
|
||||||
|
onAction?.(action.key, action);
|
||||||
|
// 如果没有确认框,则立即关闭 Dropdown
|
||||||
|
setOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const dropdownContent = (
|
||||||
|
<div className="bg-white p-2 rounded shadow-md">
|
||||||
|
<Space direction="vertical" className="w-full">
|
||||||
|
{actions.map((action) => {
|
||||||
|
if (action.confirm) {
|
||||||
|
return (
|
||||||
|
<Popconfirm
|
||||||
|
key={action.key}
|
||||||
|
title={action.confirm.title}
|
||||||
|
description={action.confirm.description}
|
||||||
|
onConfirm={() => {
|
||||||
|
onAction?.(action.key, action);
|
||||||
|
setOpen(false);
|
||||||
|
}}
|
||||||
|
okText={action.confirm.okText || "确定"}
|
||||||
|
cancelText={action.confirm.cancelText || "取消"}
|
||||||
|
okType={action.danger ? "danger" : "primary"}
|
||||||
|
styles={{ root: { zIndex: 9999 } }}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
className="w-full text-left"
|
||||||
|
danger={action.danger}
|
||||||
|
icon={action.icon}
|
||||||
|
>
|
||||||
|
{action.label}
|
||||||
|
</Button>
|
||||||
|
</Popconfirm>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
key={action.key}
|
||||||
|
className="w-full"
|
||||||
|
size="small"
|
||||||
|
type="text"
|
||||||
|
danger={action.danger}
|
||||||
|
icon={action.icon}
|
||||||
|
onClick={() => handleActionClick(action)}
|
||||||
|
>
|
||||||
|
{action.label}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dropdown
|
||||||
|
overlay={dropdownContent}
|
||||||
|
trigger={["click"]}
|
||||||
|
placement={placement}
|
||||||
|
open={open}
|
||||||
|
onOpenChange={setOpen}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
icon={<EllipsisOutlined style={{ fontSize: 24 }} />}
|
||||||
|
/>
|
||||||
|
</Dropdown>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ActionDropdown;
|
||||||
@@ -64,6 +64,7 @@ export default function AddTagPopover({
|
|||||||
open={showPopover}
|
open={showPopover}
|
||||||
trigger="click"
|
trigger="click"
|
||||||
placement="bottom"
|
placement="bottom"
|
||||||
|
onOpenChange={setShowPopover}
|
||||||
content={
|
content={
|
||||||
<div className="space-y-4 w-[300px]">
|
<div className="space-y-4 w-[300px]">
|
||||||
<h4 className="font-medium border-b pb-2 border-gray-100">
|
<h4 className="font-medium border-b pb-2 border-gray-100">
|
||||||
|
|||||||
@@ -1,12 +1,17 @@
|
|||||||
import React, { useState, useEffect, useRef } from "react";
|
import React, { useState, useEffect, useRef } from "react";
|
||||||
import { Tag, Pagination, Dropdown, Tooltip, Empty, Popover } from "antd";
|
|
||||||
import {
|
import {
|
||||||
EllipsisOutlined,
|
Tag,
|
||||||
ClockCircleOutlined,
|
Pagination,
|
||||||
StarFilled,
|
Tooltip,
|
||||||
} from "@ant-design/icons";
|
Empty,
|
||||||
|
Popover,
|
||||||
|
Menu,
|
||||||
|
Popconfirm,
|
||||||
|
} from "antd";
|
||||||
|
import { ClockCircleOutlined, StarFilled } from "@ant-design/icons";
|
||||||
import type { ItemType } from "antd/es/menu/interface";
|
import type { ItemType } from "antd/es/menu/interface";
|
||||||
import { formatDateTime } from "@/utils/unit";
|
import { formatDateTime } from "@/utils/unit";
|
||||||
|
import ActionDropdown from "./ActionDropdown";
|
||||||
|
|
||||||
interface BaseCardDataType {
|
interface BaseCardDataType {
|
||||||
id: string | number;
|
id: string | number;
|
||||||
@@ -168,6 +173,48 @@ function CardView<T extends BaseCardDataType>(props: CardViewProps<T>) {
|
|||||||
|
|
||||||
const ops = (item) =>
|
const ops = (item) =>
|
||||||
typeof operations === "function" ? operations(item) : operations;
|
typeof operations === "function" ? operations(item) : operations;
|
||||||
|
|
||||||
|
const menu = (item) => {
|
||||||
|
const ops =
|
||||||
|
typeof operations === "function" ? operations(item) : operations;
|
||||||
|
<Menu>
|
||||||
|
{ops.map((op) => {
|
||||||
|
if (op?.danger) {
|
||||||
|
return (
|
||||||
|
<Menu.Item key={op?.key} disabled icon={op?.icon}>
|
||||||
|
<Popconfirm
|
||||||
|
title="确定删除吗?"
|
||||||
|
description="此操作不可撤销"
|
||||||
|
onConfirm={op.onClick ? () => op.onClick(item) : undefined}
|
||||||
|
okText="确定"
|
||||||
|
cancelText="取消"
|
||||||
|
// 阻止事件冒泡,避免 Dropdown 关闭
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "block",
|
||||||
|
width: "100%",
|
||||||
|
color: "inherit",
|
||||||
|
}}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
{op.icon}
|
||||||
|
{op.label}
|
||||||
|
</div>
|
||||||
|
</Popconfirm>
|
||||||
|
</Menu.Item>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<Menu.Item key={op?.key} onClick={op?.onClick} icon={op?.icon}>
|
||||||
|
{op?.label}
|
||||||
|
</Menu.Item>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
</Menu>;
|
||||||
|
};
|
||||||
return (
|
return (
|
||||||
<div className="flex-overflow-hidden">
|
<div className="flex-overflow-hidden">
|
||||||
<div className="overflow-auto grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4 gap-4">
|
<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>) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{operations && (
|
{operations && (
|
||||||
<Dropdown
|
<ActionDropdown
|
||||||
trigger={["click"]}
|
actions={ops(item)}
|
||||||
menu={{
|
onAction={(key) => {
|
||||||
items: ops(item),
|
const operation = ops(item).find((op) => op.key === key);
|
||||||
onClick: ({ key }) => {
|
|
||||||
const operation = ops(item).find(
|
|
||||||
(op) => op.key === key
|
|
||||||
);
|
|
||||||
if (operation?.onClick) {
|
if (operation?.onClick) {
|
||||||
operation.onClick(item);
|
operation.onClick(item);
|
||||||
}
|
}
|
||||||
},
|
|
||||||
}}
|
}}
|
||||||
>
|
/>
|
||||||
<div className="cursor-pointer">
|
|
||||||
<EllipsisOutlined style={{ fontSize: 24 }} />
|
|
||||||
</div>
|
|
||||||
</Dropdown>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Database } from "lucide-react";
|
import { Database } from "lucide-react";
|
||||||
import { Card, Dropdown, Button, Tag, Tooltip } from "antd";
|
import { Card, Button, Tag, Tooltip, Popconfirm } from "antd";
|
||||||
import type { ItemType } from "antd/es/menu/interface";
|
import type { ItemType } from "antd/es/menu/interface";
|
||||||
import AddTagPopover from "./AddTagPopover";
|
import AddTagPopover from "./AddTagPopover";
|
||||||
|
import ActionDropdown from "./ActionDropdown";
|
||||||
|
|
||||||
interface StatisticItem {
|
interface StatisticItem {
|
||||||
icon: React.ReactNode;
|
icon: React.ReactNode;
|
||||||
@@ -100,22 +101,39 @@ function DetailHeader<T>({
|
|||||||
{operations.map((op) => {
|
{operations.map((op) => {
|
||||||
if (op.isDropdown) {
|
if (op.isDropdown) {
|
||||||
return (
|
return (
|
||||||
<Dropdown
|
<ActionDropdown
|
||||||
|
actions={op?.items}
|
||||||
|
onAction={op?.onMenuClick}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (op.confirm) {
|
||||||
|
return (
|
||||||
|
<Tooltip key={op.key} title={op.label}>
|
||||||
|
<Popconfirm
|
||||||
key={op.key}
|
key={op.key}
|
||||||
menu={{
|
title={op.confirm.title}
|
||||||
items: op?.items as ItemType[],
|
description={op.confirm.description}
|
||||||
onClick: op?.onMenuClick,
|
onConfirm={() => {
|
||||||
|
op?.onClick();
|
||||||
}}
|
}}
|
||||||
|
okText={op.confirm.okText || "确定"}
|
||||||
|
cancelText={op.confirm.cancelText || "取消"}
|
||||||
|
okType={op.danger ? "danger" : "primary"}
|
||||||
|
overlayStyle={{ zIndex: 9999 }}
|
||||||
>
|
>
|
||||||
<Tooltip title={op.label}>
|
<Button icon={op.icon} danger={op.danger} />
|
||||||
<Button icon={op.icon} />
|
</Popconfirm>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Dropdown>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Tooltip key={op.key} title={op.label}>
|
<Tooltip key={op.key} title={op.label}>
|
||||||
<Button {...op} />
|
<Button
|
||||||
|
icon={op.icon}
|
||||||
|
danger={op.danger}
|
||||||
|
onClick={op.onClick}
|
||||||
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -3,23 +3,27 @@
|
|||||||
// const { fetchData, startPolling, stopPolling, isPolling } = useFetchData(
|
// const { fetchData, startPolling, stopPolling, isPolling } = useFetchData(
|
||||||
// fetchFunction,
|
// fetchFunction,
|
||||||
// mapFunction,
|
// mapFunction,
|
||||||
// 5000 // 5秒轮询一次,默认30秒
|
// 5000, // 5秒轮询一次,默认30秒
|
||||||
// false // 是否自动开始轮询,默认 true
|
// true, // 是否自动开始轮询,默认 true
|
||||||
|
// [fetchStatistics, fetchOtherData] // 额外的轮询函数数组
|
||||||
// );
|
// );
|
||||||
//
|
//
|
||||||
// startPolling(); // 开始轮询
|
// startPolling(); // 开始轮询
|
||||||
// stopPolling(); // 停止轮询
|
// stopPolling(); // 停止轮询
|
||||||
// 手动调用 fetchData() 时,如果正在轮询,会重新开始轮询计时
|
// 手动调用 fetchData() 时,如果正在轮询,会重新开始轮询计时
|
||||||
|
// 轮询时会同时执行主要的 fetchFunction 和所有额外的轮询函数
|
||||||
import { useState, useRef, useEffect, useCallback } from "react";
|
import { useState, useRef, useEffect, useCallback } from "react";
|
||||||
import { useDebouncedEffect } from "./useDebouncedEffect";
|
import { useDebouncedEffect } from "./useDebouncedEffect";
|
||||||
import Loading from "@/utils/loading";
|
import Loading from "@/utils/loading";
|
||||||
import { App } from "antd";
|
import { App } from "antd";
|
||||||
|
import { AnyObject } from "antd/es/_util/type";
|
||||||
|
|
||||||
export default function useFetchData<T>(
|
export default function useFetchData<T>(
|
||||||
fetchFunc: (params?: any) => Promise<any>,
|
fetchFunc: (params?: any) => Promise<any>,
|
||||||
mapDataFunc: (data: any) => T = (data) => data as T,
|
mapDataFunc: (data: AnyObject) => T = (data) => data as T,
|
||||||
pollingInterval: number = 30000, // 默认30秒轮询一次
|
pollingInterval: number = 30000, // 默认30秒轮询一次
|
||||||
autoRefresh: boolean = true
|
autoRefresh: boolean = true,
|
||||||
|
additionalPollingFuncs: (() => Promise<any>)[] = [] // 额外的轮询函数
|
||||||
) {
|
) {
|
||||||
const { message } = App.useApp();
|
const { message } = App.useApp();
|
||||||
|
|
||||||
@@ -97,7 +101,9 @@ export default function useFetchData<T>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { data } = await fetchFunc({
|
// 同时执行主要数据获取和额外的轮询函数
|
||||||
|
const promises = [
|
||||||
|
fetchFunc({
|
||||||
...filter,
|
...filter,
|
||||||
...extraParams,
|
...extraParams,
|
||||||
keyword,
|
keyword,
|
||||||
@@ -106,7 +112,13 @@ export default function useFetchData<T>(
|
|||||||
tags: filter?.tags?.length ? filter.tags.join(",") : undefined,
|
tags: filter?.tags?.length ? filter.tags.join(",") : undefined,
|
||||||
page: current - 1,
|
page: current - 1,
|
||||||
size: pageSize,
|
size: pageSize,
|
||||||
});
|
}),
|
||||||
|
...additionalPollingFuncs.map((func) => func()),
|
||||||
|
];
|
||||||
|
|
||||||
|
const results = await Promise.all(promises);
|
||||||
|
const { data } = results[0]; // 主要数据结果
|
||||||
|
|
||||||
setPagination((prev) => ({
|
setPagination((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
total: data?.totalElements || 0,
|
total: data?.totalElements || 0,
|
||||||
@@ -146,6 +158,7 @@ export default function useFetchData<T>(
|
|||||||
clearPollingTimer,
|
clearPollingTimer,
|
||||||
pollingInterval,
|
pollingInterval,
|
||||||
message,
|
message,
|
||||||
|
additionalPollingFuncs,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,411 +0,0 @@
|
|||||||
/* PreciseDragDrop.css */
|
|
||||||
.precise-drag-drop {
|
|
||||||
max-width: 1200px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 20px;
|
|
||||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header {
|
|
||||||
text-align: center;
|
|
||||||
margin-bottom: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header h1 {
|
|
||||||
color: #2c3e50;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header p {
|
|
||||||
color: #7f8c8d;
|
|
||||||
font-size: 1.1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.containers {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
gap: 30px;
|
|
||||||
margin-bottom: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
background: white;
|
|
||||||
border-radius: 12px;
|
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
|
||||||
overflow: hidden;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
border: 2px solid transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container.drag-over {
|
|
||||||
border-color: #3498db;
|
|
||||||
background-color: #f8fafc;
|
|
||||||
transform: scale(1.02);
|
|
||||||
}
|
|
||||||
|
|
||||||
.container-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
padding: 20px;
|
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container-header h2 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 1.4rem;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.count {
|
|
||||||
background: rgba(255, 255, 255, 0.2);
|
|
||||||
padding: 6px 12px;
|
|
||||||
border-radius: 20px;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-actions {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.clear-btn {
|
|
||||||
background: rgba(255, 255, 255, 0.2);
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
|
||||||
color: white;
|
|
||||||
padding: 6px 12px;
|
|
||||||
border-radius: 6px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.clear-btn:hover {
|
|
||||||
background: rgba(255, 255, 255, 0.3);
|
|
||||||
transform: translateY(-1px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.items-list {
|
|
||||||
padding: 20px;
|
|
||||||
min-height: 500px;
|
|
||||||
max-height: 600px;
|
|
||||||
overflow-y: auto;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
padding: 16px;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
background: white;
|
|
||||||
border-radius: 8px;
|
|
||||||
border-left: 4px solid var(--item-color);
|
|
||||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08);
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
cursor: grab;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item:hover {
|
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
.item.dragging {
|
|
||||||
opacity: 0.6;
|
|
||||||
cursor: grabbing;
|
|
||||||
transform: rotate(3deg) scale(1.05);
|
|
||||||
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.item.drag-over.insert-above {
|
|
||||||
border-top: 2px dashed var(--item-color);
|
|
||||||
margin-top: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item.drag-over.insert-below {
|
|
||||||
border-bottom: 2px dashed var(--item-color);
|
|
||||||
margin-bottom: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item-content {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item-index {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 28px;
|
|
||||||
height: 28px;
|
|
||||||
background-color: var(--item-color);
|
|
||||||
color: white;
|
|
||||||
border-radius: 50%;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
font-weight: bold;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item-icon {
|
|
||||||
font-size: 1.3rem;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item-info {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 4px;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item-title {
|
|
||||||
font-weight: 500;
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.priority-tag {
|
|
||||||
padding: 2px 8px;
|
|
||||||
border-radius: 12px;
|
|
||||||
font-size: 0.7rem;
|
|
||||||
font-weight: 600;
|
|
||||||
text-transform: uppercase;
|
|
||||||
width: fit-content;
|
|
||||||
}
|
|
||||||
|
|
||||||
.priority-high {
|
|
||||||
background: #ffebee;
|
|
||||||
color: #c62828;
|
|
||||||
}
|
|
||||||
|
|
||||||
.priority-medium {
|
|
||||||
background: #fff3e0;
|
|
||||||
color: #ef6c00;
|
|
||||||
}
|
|
||||||
|
|
||||||
.priority-low {
|
|
||||||
background: #e8f5e8;
|
|
||||||
color: #2e7d32;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item-type {
|
|
||||||
background: #f1f3f4;
|
|
||||||
padding: 4px 8px;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
color: #666;
|
|
||||||
text-transform: capitalize;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item-actions {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.drag-handle {
|
|
||||||
color: #bdc3c7;
|
|
||||||
font-size: 16px;
|
|
||||||
cursor: grab;
|
|
||||||
padding: 8px;
|
|
||||||
user-select: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.drag-handle:hover {
|
|
||||||
color: #7f8c8d;
|
|
||||||
background: #f5f5f5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-state {
|
|
||||||
text-align: center;
|
|
||||||
padding: 80px 20px;
|
|
||||||
color: #95a5a6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-state p {
|
|
||||||
margin: 0 0 8px 0;
|
|
||||||
font-size: 1.1rem;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-state span {
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 插入位置指示器 */
|
|
||||||
.insert-indicator {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
padding: 8px 0;
|
|
||||||
margin: 4px 0;
|
|
||||||
opacity: 0.8;
|
|
||||||
animation: pulse 1.5s infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.insert-indicator.above {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.insert-indicator.below {
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.indicator-line {
|
|
||||||
flex: 1;
|
|
||||||
height: 2px;
|
|
||||||
background: linear-gradient(90deg, transparent, var(--item-color, #3498db), transparent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.indicator-arrow {
|
|
||||||
color: var(--item-color, #3498db);
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
padding: 0 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes pulse {
|
|
||||||
0%, 100% { opacity: 0.6; }
|
|
||||||
50% { opacity: 1; }
|
|
||||||
}
|
|
||||||
|
|
||||||
.instructions {
|
|
||||||
background: #f8f9fa;
|
|
||||||
padding: 25px;
|
|
||||||
border-radius: 12px;
|
|
||||||
border-left: 4px solid #3498db;
|
|
||||||
}
|
|
||||||
|
|
||||||
.instructions h3 {
|
|
||||||
margin-top: 0;
|
|
||||||
color: #2c3e50;
|
|
||||||
text-align: center;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
font-size: 1.3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.instruction-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
|
||||||
gap: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.instruction {
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 15px;
|
|
||||||
padding: 20px;
|
|
||||||
background: white;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.instruction:hover {
|
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.instruction .icon {
|
|
||||||
font-size: 1.8rem;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.instruction strong {
|
|
||||||
display: block;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
color: #2c3e50;
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.instruction p {
|
|
||||||
margin: 0;
|
|
||||||
color: #7f8c8d;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
line-height: 1.4;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 动画效果 */
|
|
||||||
@keyframes slideIn {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(10px);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.item {
|
|
||||||
animation: slideIn 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 滚动条样式 */
|
|
||||||
.items-list::-webkit-scrollbar {
|
|
||||||
width: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.items-list::-webkit-scrollbar-track {
|
|
||||||
background: #f1f1f1;
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.items-list::-webkit-scrollbar-thumb {
|
|
||||||
background: #c1c1c1;
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.items-list::-webkit-scrollbar-thumb:hover {
|
|
||||||
background: #a8a8a8;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 响应式设计 */
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.containers {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
gap: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.precise-drag-drop {
|
|
||||||
padding: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.instruction-grid {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container-header {
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 10px;
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-actions {
|
|
||||||
align-self: flex-end;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item-content {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item-info {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,430 +0,0 @@
|
|||||||
import React, { useState } from "react";
|
|
||||||
import "./DragDrop.css";
|
|
||||||
|
|
||||||
const PreciseDragDrop = () => {
|
|
||||||
// 初始数据
|
|
||||||
const [leftItems, setLeftItems] = useState([
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
title: "需求分析",
|
|
||||||
type: "analysis",
|
|
||||||
color: "#4CAF50",
|
|
||||||
priority: "high",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
title: "UI设计",
|
|
||||||
type: "design",
|
|
||||||
color: "#2196F3",
|
|
||||||
priority: "medium",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
title: "前端开发",
|
|
||||||
type: "development",
|
|
||||||
color: "#FF9800",
|
|
||||||
priority: "high",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 4,
|
|
||||||
title: "后端开发",
|
|
||||||
type: "development",
|
|
||||||
color: "#9C27B0",
|
|
||||||
priority: "high",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 5,
|
|
||||||
title: "功能测试",
|
|
||||||
type: "testing",
|
|
||||||
color: "#3F51B5",
|
|
||||||
priority: "medium",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 6,
|
|
||||||
title: "部署上线",
|
|
||||||
type: "deployment",
|
|
||||||
color: "#009688",
|
|
||||||
priority: "low",
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
const [rightItems, setRightItems] = useState([
|
|
||||||
{
|
|
||||||
id: 7,
|
|
||||||
title: "项目启动",
|
|
||||||
type: "planning",
|
|
||||||
color: "#E91E63",
|
|
||||||
priority: "high",
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
const [draggingItem, setDraggingItem] = useState(null);
|
|
||||||
const [insertPosition, setInsertPosition] = useState(null); // 'above' 或 'below'
|
|
||||||
|
|
||||||
// 处理拖拽开始
|
|
||||||
const handleDragStart = (e, item, source) => {
|
|
||||||
setDraggingItem({ ...item, source });
|
|
||||||
e.dataTransfer.effectAllowed = "move";
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
e.target.classList.add("dragging");
|
|
||||||
}, 0);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 处理拖拽结束
|
|
||||||
const handleDragEnd = (e) => {
|
|
||||||
setDraggingItem(null);
|
|
||||||
setInsertPosition(null);
|
|
||||||
e.target.classList.remove("dragging");
|
|
||||||
};
|
|
||||||
|
|
||||||
// 处理容器拖拽经过
|
|
||||||
const handleContainerDragOver = (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
};
|
|
||||||
|
|
||||||
// 处理容器拖拽离开
|
|
||||||
const handleContainerDragLeave = (e) => {
|
|
||||||
if (!e.currentTarget.contains(e.relatedTarget)) {
|
|
||||||
setInsertPosition(null);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 处理项目拖拽经过(用于精确插入)
|
|
||||||
const handleItemDragOver = (e, itemId) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
|
|
||||||
const rect = e.currentTarget.getBoundingClientRect();
|
|
||||||
const mouseY = e.clientY;
|
|
||||||
const elementMiddle = rect.top + rect.height;
|
|
||||||
|
|
||||||
// 判断鼠标在元素的上半部分还是下半部分
|
|
||||||
const newPosition = mouseY < elementMiddle ? "above" : "below";
|
|
||||||
|
|
||||||
setInsertPosition(newPosition);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 处理项目拖拽离开
|
|
||||||
const handleItemDragLeave = (e) => {
|
|
||||||
if (!e.currentTarget.contains(e.relatedTarget)) {
|
|
||||||
setInsertPosition(null);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 处理放置到右侧容器空白区域
|
|
||||||
const handleDropToRightContainer = (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
if (!draggingItem) return;
|
|
||||||
|
|
||||||
// 如果是从左侧拖拽过来的
|
|
||||||
if (draggingItem.source === "left") {
|
|
||||||
// 检查是否已存在
|
|
||||||
const exists = rightItems.some((item) => item.id === draggingItem.id);
|
|
||||||
if (!exists) {
|
|
||||||
setRightItems((prev) => [
|
|
||||||
...prev,
|
|
||||||
{
|
|
||||||
...draggingItem,
|
|
||||||
source: "right",
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
setLeftItems((prev) =>
|
|
||||||
prev.filter((item) => item.id !== draggingItem.id)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
resetDragState();
|
|
||||||
};
|
|
||||||
|
|
||||||
// 处理放置到右侧容器的特定位置
|
|
||||||
const handleDropToRightItem = (e, targetItemId) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
|
|
||||||
if (!draggingItem) return;
|
|
||||||
|
|
||||||
// 从左侧拖拽到右侧的精确插入
|
|
||||||
if (draggingItem.source === "left") {
|
|
||||||
const targetIndex = rightItems.findIndex(
|
|
||||||
(item) => item.id === targetItemId
|
|
||||||
);
|
|
||||||
|
|
||||||
if (targetIndex !== -1) {
|
|
||||||
const insertIndex =
|
|
||||||
insertPosition === "above" ? targetIndex : targetIndex + 1;
|
|
||||||
|
|
||||||
// 检查是否已存在
|
|
||||||
const exists = rightItems.some((item) => item.id === draggingItem.id);
|
|
||||||
if (!exists) {
|
|
||||||
const newRightItems = [...rightItems];
|
|
||||||
newRightItems.splice(insertIndex, 0, {
|
|
||||||
...draggingItem,
|
|
||||||
source: "right",
|
|
||||||
});
|
|
||||||
|
|
||||||
setRightItems(newRightItems);
|
|
||||||
setLeftItems((prev) =>
|
|
||||||
prev.filter((item) => item.id !== draggingItem.id)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 右侧容器内的重新排序
|
|
||||||
else if (draggingItem.source === "right") {
|
|
||||||
const draggedIndex = rightItems.findIndex(
|
|
||||||
(item) => item.id === draggingItem.id
|
|
||||||
);
|
|
||||||
const targetIndex = rightItems.findIndex(
|
|
||||||
(item) => item.id === targetItemId
|
|
||||||
);
|
|
||||||
|
|
||||||
if (
|
|
||||||
draggedIndex !== -1 &&
|
|
||||||
targetIndex !== -1 &&
|
|
||||||
draggedIndex !== targetIndex
|
|
||||||
) {
|
|
||||||
const newItems = [...rightItems];
|
|
||||||
const [draggedItem] = newItems.splice(draggedIndex, 1);
|
|
||||||
|
|
||||||
// 计算正确的插入位置
|
|
||||||
let insertIndex =
|
|
||||||
insertPosition === "above" ? targetIndex : targetIndex + 1;
|
|
||||||
if (draggedIndex < insertIndex) {
|
|
||||||
insertIndex--; // 调整插入位置,因为已经移除了原元素
|
|
||||||
}
|
|
||||||
|
|
||||||
newItems.splice(insertIndex, 0, draggedItem);
|
|
||||||
setRightItems(newItems);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
resetDragState();
|
|
||||||
};
|
|
||||||
|
|
||||||
// 处理拖拽回左侧容器
|
|
||||||
const handleDropToLeft = (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
if (!draggingItem || draggingItem.source !== "right") return;
|
|
||||||
|
|
||||||
setRightItems((prev) => prev.filter((item) => item.id !== draggingItem.id));
|
|
||||||
setLeftItems((prev) => [
|
|
||||||
...prev,
|
|
||||||
{
|
|
||||||
...draggingItem,
|
|
||||||
source: "left",
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
resetDragState();
|
|
||||||
};
|
|
||||||
|
|
||||||
// 重置拖拽状态
|
|
||||||
const resetDragState = () => {
|
|
||||||
setDraggingItem(null);
|
|
||||||
setInsertPosition(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 清空右侧容器
|
|
||||||
const clearRightContainer = () => {
|
|
||||||
setLeftItems((prev) => [
|
|
||||||
...prev,
|
|
||||||
...rightItems.map((item) => ({
|
|
||||||
...item,
|
|
||||||
source: "left",
|
|
||||||
})),
|
|
||||||
]);
|
|
||||||
setRightItems([]);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 获取类型图标
|
|
||||||
const getTypeIcon = (type) => {
|
|
||||||
switch (type) {
|
|
||||||
case "analysis":
|
|
||||||
return "📊";
|
|
||||||
case "design":
|
|
||||||
return "🎨";
|
|
||||||
case "development":
|
|
||||||
return "💻";
|
|
||||||
case "testing":
|
|
||||||
return "🧪";
|
|
||||||
case "deployment":
|
|
||||||
return "🚀";
|
|
||||||
case "planning":
|
|
||||||
return "📋";
|
|
||||||
default:
|
|
||||||
return "📌";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 获取优先级标签
|
|
||||||
const getPriorityLabel = (priority) => {
|
|
||||||
switch (priority) {
|
|
||||||
case "high":
|
|
||||||
return { label: "高优先级", class: "priority-high" };
|
|
||||||
case "medium":
|
|
||||||
return { label: "中优先级", class: "priority-medium" };
|
|
||||||
case "low":
|
|
||||||
return { label: "低优先级", class: "priority-low" };
|
|
||||||
default:
|
|
||||||
return { label: "普通", class: "priority-medium" };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="precise-drag-drop">
|
|
||||||
<div className="header">
|
|
||||||
<h1>精确位置拖拽排序</h1>
|
|
||||||
<p>拖拽时悬停在项目上方或下方可选择精确插入位置</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="containers">
|
|
||||||
{/* 左侧容器 - 待办事项 */}
|
|
||||||
<div
|
|
||||||
className={`container left-container `}
|
|
||||||
onDragOver={(e) => handleContainerDragOver(e, "left")}
|
|
||||||
onDragLeave={handleContainerDragLeave}
|
|
||||||
onDrop={handleDropToLeft}
|
|
||||||
>
|
|
||||||
<div className="container-header">
|
|
||||||
<h2>📋 待办事项</h2>
|
|
||||||
<span className="count">{leftItems.length} 项</span>
|
|
||||||
</div>
|
|
||||||
<div className="items-list">
|
|
||||||
{leftItems.map((item) => (
|
|
||||||
<div
|
|
||||||
key={item.id}
|
|
||||||
className="item"
|
|
||||||
draggable
|
|
||||||
onDragStart={(e) => handleDragStart(e, item, "left")}
|
|
||||||
onDragEnd={handleDragEnd}
|
|
||||||
style={{ "--item-color": item.color }}
|
|
||||||
>
|
|
||||||
<div className="item-content">
|
|
||||||
<span className="item-icon">{getTypeIcon(item.type)}</span>
|
|
||||||
<div className="item-info">
|
|
||||||
<span className="item-title">{item.title}</span>
|
|
||||||
<span
|
|
||||||
className={`priority-tag ${
|
|
||||||
getPriorityLabel(item.priority).class
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{getPriorityLabel(item.priority).label}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="item-type">{item.type}</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
{leftItems.length === 0 && (
|
|
||||||
<div className="empty-state">
|
|
||||||
<p>🎉 所有任务已完成!</p>
|
|
||||||
<span>从右侧拖拽项目回来重新安排</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 右侧容器 - 进行中的任务 */}
|
|
||||||
<div
|
|
||||||
className={`container right-container`}
|
|
||||||
onDragOver={(e) => handleContainerDragOver(e, "right")}
|
|
||||||
onDragLeave={handleContainerDragLeave}
|
|
||||||
onDrop={handleDropToRightContainer}
|
|
||||||
>
|
|
||||||
<div className="container-header">
|
|
||||||
<h2>🚀 进行中的任务</h2>
|
|
||||||
<div className="header-actions">
|
|
||||||
<span className="count">{rightItems.length} 项</span>
|
|
||||||
{rightItems.length > 0 && (
|
|
||||||
<button className="clear-btn" onClick={clearRightContainer}>
|
|
||||||
清空所有
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="items-list">
|
|
||||||
{rightItems.length === 0 ? (
|
|
||||||
<div className="empty-state">
|
|
||||||
<p>📥 暂无进行中的任务</p>
|
|
||||||
<span>从左侧拖拽项目过来开始工作</span>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
rightItems.map((item, index) => (
|
|
||||||
<div
|
|
||||||
key={item.id}
|
|
||||||
className={`item `}
|
|
||||||
draggable
|
|
||||||
onDragStart={(e) => handleDragStart(e, item, "right")}
|
|
||||||
onDragEnd={handleDragEnd}
|
|
||||||
onDragOver={(e) => handleItemDragOver(e, item.id)}
|
|
||||||
onDragLeave={handleItemDragLeave}
|
|
||||||
onDrop={(e) => handleDropToRightItem(e, item.id)}
|
|
||||||
style={{ "--item-color": item.color }}
|
|
||||||
>
|
|
||||||
<div className="item-content">
|
|
||||||
<span className="item-index">{index + 1}</span>
|
|
||||||
<span className="item-icon">{getTypeIcon(item.type)}</span>
|
|
||||||
<div className="item-info">
|
|
||||||
<span className="item-title">{item.title}</span>
|
|
||||||
<span
|
|
||||||
className={`priority-tag ${
|
|
||||||
getPriorityLabel(item.priority).class
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{getPriorityLabel(item.priority).label}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="item-actions">
|
|
||||||
<span className="drag-handle">⋮⋮</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="instructions">
|
|
||||||
<h3>🎯 操作指南</h3>
|
|
||||||
<div className="instruction-grid">
|
|
||||||
<div className="instruction">
|
|
||||||
<span className="icon">🎯</span>
|
|
||||||
<div>
|
|
||||||
<strong>精确插入</strong>
|
|
||||||
<p>拖拽时悬停在项目上方或下方选择插入位置</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="instruction">
|
|
||||||
<span className="icon">🔄</span>
|
|
||||||
<div>
|
|
||||||
<strong>重新排序</strong>
|
|
||||||
<p>在右侧容器内拖拽调整任务顺序</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="instruction">
|
|
||||||
<span className="icon">📤</span>
|
|
||||||
<div>
|
|
||||||
<strong>移回待办</strong>
|
|
||||||
<p>从右侧拖拽任务回左侧容器</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="instruction">
|
|
||||||
<span className="icon">🧹</span>
|
|
||||||
<div>
|
|
||||||
<strong>批量操作</strong>
|
|
||||||
<p>使用"清空所有"按钮快速重置</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default PreciseDragDrop;
|
|
||||||
@@ -4,7 +4,7 @@ import { ArrowLeft } from "lucide-react";
|
|||||||
import { Button, Form, App } from "antd";
|
import { Button, Form, App } from "antd";
|
||||||
import { Link, useNavigate } from "react-router";
|
import { Link, useNavigate } from "react-router";
|
||||||
import { createDatasetUsingPost } from "../dataset.api";
|
import { createDatasetUsingPost } from "../dataset.api";
|
||||||
import { DatasetType, DataSource } from "../dataset.model";
|
import { DatasetType } from "../dataset.model";
|
||||||
import BasicInformation from "./components/BasicInformation";
|
import BasicInformation from "./components/BasicInformation";
|
||||||
|
|
||||||
export default function DatasetCreate() {
|
export default function DatasetCreate() {
|
||||||
@@ -69,7 +69,11 @@ export default function DatasetCreate() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2 justify-end p-6 border-top">
|
<div className="flex gap-2 justify-end p-6 border-top">
|
||||||
<Button onClick={() => navigate("/data/management")}>取消</Button>
|
<Button onClick={() => navigate("/data/management")}>取消</Button>
|
||||||
<Button type="primary" onClick={handleSubmit}>
|
<Button
|
||||||
|
type="primary"
|
||||||
|
disabled={!newDataset.name || !newDataset.datasetType}
|
||||||
|
onClick={handleSubmit}
|
||||||
|
>
|
||||||
确定
|
确定
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -10,11 +10,12 @@ import {
|
|||||||
import DetailHeader from "@/components/DetailHeader";
|
import DetailHeader from "@/components/DetailHeader";
|
||||||
import { mapDataset, datasetTypeMap } from "../dataset.const";
|
import { mapDataset, datasetTypeMap } from "../dataset.const";
|
||||||
import type { Dataset } from "@/pages/DataManagement/dataset.model";
|
import type { Dataset } from "@/pages/DataManagement/dataset.model";
|
||||||
import { Link, useParams } from "react-router";
|
import { Link, useNavigate, useParams } from "react-router";
|
||||||
import { useFilesOperation } from "../hooks";
|
import { useFilesOperation } from "./useFilesOperation";
|
||||||
import {
|
import {
|
||||||
createDatasetTagUsingPost,
|
createDatasetTagUsingPost,
|
||||||
downloadFile,
|
deleteDatasetByIdUsingDelete,
|
||||||
|
downloadDatasetUsingGet,
|
||||||
queryDatasetByIdUsingGet,
|
queryDatasetByIdUsingGet,
|
||||||
queryDatasetTagsUsingGet,
|
queryDatasetTagsUsingGet,
|
||||||
updateDatasetByIdUsingPut,
|
updateDatasetByIdUsingPut,
|
||||||
@@ -43,6 +44,7 @@ const tabList = [
|
|||||||
|
|
||||||
export default function DatasetDetail() {
|
export default function DatasetDetail() {
|
||||||
const { id } = useParams(); // 获取动态路由参数
|
const { id } = useParams(); // 获取动态路由参数
|
||||||
|
const navigate = useNavigate();
|
||||||
const [activeTab, setActiveTab] = useState("overview");
|
const [activeTab, setActiveTab] = useState("overview");
|
||||||
const { message } = App.useApp();
|
const { message } = App.useApp();
|
||||||
const [showEditDialog, setShowEditDialog] = useState(false);
|
const [showEditDialog, setShowEditDialog] = useState(false);
|
||||||
@@ -79,10 +81,16 @@ export default function DatasetDetail() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleDownload = async () => {
|
const handleDownload = async () => {
|
||||||
await downloadFile(dataset.id);
|
await downloadDatasetUsingGet(dataset.id);
|
||||||
message.success("文件下载成功");
|
message.success("文件下载成功");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleDeleteDataset = async () => {
|
||||||
|
await deleteDatasetByIdUsingDelete(dataset.id);
|
||||||
|
navigate("/data/management");
|
||||||
|
message.success("数据集删除成功");
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const refreshDataset = () => {
|
const refreshDataset = () => {
|
||||||
fetchDataset();
|
fetchDataset();
|
||||||
@@ -166,11 +174,16 @@ export default function DatasetDetail() {
|
|||||||
key: "delete",
|
key: "delete",
|
||||||
label: "删除",
|
label: "删除",
|
||||||
danger: true,
|
danger: true,
|
||||||
icon: <DeleteOutlined />,
|
confirm: {
|
||||||
onClick: () => {
|
title: "确认删除该数据集?",
|
||||||
console.log("delete dataset");
|
description: "删除后该数据集将无法恢复,请谨慎操作。",
|
||||||
|
okText: "删除",
|
||||||
|
cancelText: "取消",
|
||||||
|
okType: "danger",
|
||||||
|
},
|
||||||
|
icon: <DeleteOutlined />,
|
||||||
|
onClick: handleDeleteDataset,
|
||||||
},
|
},
|
||||||
}
|
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,10 +1,21 @@
|
|||||||
import { Select, Input, Form, Radio, Modal, Button } from "antd";
|
import {
|
||||||
|
Select,
|
||||||
|
Input,
|
||||||
|
Form,
|
||||||
|
Radio,
|
||||||
|
Modal,
|
||||||
|
Button,
|
||||||
|
App,
|
||||||
|
UploadFile,
|
||||||
|
} from "antd";
|
||||||
|
import { InboxOutlined } from "@ant-design/icons";
|
||||||
import { dataSourceOptions } from "../../dataset.const";
|
import { dataSourceOptions } from "../../dataset.const";
|
||||||
import { Dataset, DataSource } from "../../dataset.model";
|
import { Dataset, DataSource } from "../../dataset.model";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { queryTasksUsingGet } from "@/pages/DataCollection/collection.apis";
|
import { queryTasksUsingGet } from "@/pages/DataCollection/collection.apis";
|
||||||
import { useImportFile } from "../../hooks";
|
|
||||||
import { updateDatasetByIdUsingPut } from "../../dataset.api";
|
import { updateDatasetByIdUsingPut } from "../../dataset.api";
|
||||||
|
import { sliceFile } from "@/utils/file.util";
|
||||||
|
import Dragger from "antd/es/upload/Dragger";
|
||||||
|
|
||||||
export default function ImportConfiguration({
|
export default function ImportConfiguration({
|
||||||
data,
|
data,
|
||||||
@@ -15,16 +26,52 @@ export default function ImportConfiguration({
|
|||||||
data?: Dataset;
|
data?: Dataset;
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onRefresh?: () => void;
|
onRefresh?: (showMessage?: boolean) => void;
|
||||||
}) {
|
}) {
|
||||||
|
const { message } = App.useApp();
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const [collectionOptions, setCollectionOptions] = useState([]);
|
const [collectionOptions, setCollectionOptions] = useState([]);
|
||||||
const [importConfig, setImportConfig] = useState<any>({
|
const [importConfig, setImportConfig] = useState<any>({
|
||||||
source: DataSource.UPLOAD,
|
source: DataSource.UPLOAD,
|
||||||
});
|
});
|
||||||
const { importFileRender, handleUpload } = useImportFile();
|
|
||||||
|
|
||||||
// 获取归集任务列表
|
const [fileList, setFileList] = useState<UploadFile[]>([]);
|
||||||
|
const fileSliceList = useMemo(() => {
|
||||||
|
const sliceList = fileList.map((file) => {
|
||||||
|
const slices = sliceFile(file);
|
||||||
|
return { originFile: file, slices, name: file.name, size: file.size };
|
||||||
|
});
|
||||||
|
return sliceList;
|
||||||
|
}, [fileList]);
|
||||||
|
|
||||||
|
// 本地上传文件相关逻辑
|
||||||
|
|
||||||
|
const resetFiles = () => {
|
||||||
|
setFileList([]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpload = async (dataset: Dataset) => {
|
||||||
|
const formData = new FormData();
|
||||||
|
fileList.forEach((file) => {
|
||||||
|
formData.append("file", file);
|
||||||
|
});
|
||||||
|
window.dispatchEvent(
|
||||||
|
new CustomEvent("upload:dataset", {
|
||||||
|
detail: { dataset, files: fileSliceList },
|
||||||
|
})
|
||||||
|
);
|
||||||
|
resetFiles();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBeforeUpload = (_, files: UploadFile[]) => {
|
||||||
|
setFileList([...fileList, ...files]);
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveFile = (file: UploadFile) => {
|
||||||
|
setFileList((prev) => prev.filter((f) => f.uid !== file.uid));
|
||||||
|
};
|
||||||
|
|
||||||
const fetchCollectionTasks = async () => {
|
const fetchCollectionTasks = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await queryTasksUsingGet({ page: 0, size: 100 });
|
const res = await queryTasksUsingGet({ page: 0, size: 100 });
|
||||||
@@ -40,6 +87,8 @@ export default function ImportConfiguration({
|
|||||||
|
|
||||||
const resetState = () => {
|
const resetState = () => {
|
||||||
form.resetFields();
|
form.resetFields();
|
||||||
|
setFileList([]);
|
||||||
|
form.setFieldsValue({ files: null });
|
||||||
setImportConfig({ source: DataSource.UPLOAD });
|
setImportConfig({ source: DataSource.UPLOAD });
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -51,13 +100,16 @@ export default function ImportConfiguration({
|
|||||||
...importConfig,
|
...importConfig,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
resetState();
|
message.success("数据已更新");
|
||||||
onRefresh?.();
|
onRefresh?.(false);
|
||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (open) fetchCollectionTasks();
|
if (open) {
|
||||||
|
resetState();
|
||||||
|
fetchCollectionTasks();
|
||||||
|
}
|
||||||
}, [open]);
|
}, [open]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -65,12 +117,19 @@ export default function ImportConfiguration({
|
|||||||
title="导入数据"
|
title="导入数据"
|
||||||
open={open}
|
open={open}
|
||||||
width={600}
|
width={600}
|
||||||
onCancel={onClose}
|
onCancel={() => {
|
||||||
|
onClose();
|
||||||
|
resetState();
|
||||||
|
}}
|
||||||
maskClosable={false}
|
maskClosable={false}
|
||||||
footer={
|
footer={
|
||||||
<>
|
<>
|
||||||
<Button onClick={onClose}>取消</Button>
|
<Button onClick={onClose}>取消</Button>
|
||||||
<Button type="primary" onClick={handleImportData}>
|
<Button
|
||||||
|
type="primary"
|
||||||
|
disabled={!fileList?.length && !importConfig.dataSource}
|
||||||
|
onClick={handleImportData}
|
||||||
|
>
|
||||||
确定
|
确定
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
@@ -132,6 +191,7 @@ export default function ImportConfiguration({
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* obs import */}
|
{/* obs import */}
|
||||||
{importConfig?.source === DataSource.OBS && (
|
{importConfig?.source === DataSource.OBS && (
|
||||||
<div className="grid grid-cols-2 gap-3 p-4 bg-blue-50 rounded-lg">
|
<div className="grid grid-cols-2 gap-3 p-4 bg-blue-50 rounded-lg">
|
||||||
@@ -185,7 +245,18 @@ export default function ImportConfiguration({
|
|||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
{importFileRender()}
|
<Dragger
|
||||||
|
className="w-full"
|
||||||
|
onRemove={handleRemoveFile}
|
||||||
|
beforeUpload={handleBeforeUpload}
|
||||||
|
multiple
|
||||||
|
>
|
||||||
|
<p className="ant-upload-drag-icon">
|
||||||
|
<InboxOutlined />
|
||||||
|
</p>
|
||||||
|
<p className="ant-upload-text">本地文件上传</p>
|
||||||
|
<p className="ant-upload-hint">拖拽文件到此处或点击选择文件</p>
|
||||||
|
</Dragger>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -82,11 +82,6 @@ export default function Overview({ dataset, filesOperation }) {
|
|||||||
label: "更新时间",
|
label: "更新时间",
|
||||||
children: dataset.updatedAt,
|
children: dataset.updatedAt,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
key: "dataSource",
|
|
||||||
label: "数据源",
|
|
||||||
children: dataset.dataSource || "未知",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
key: "description",
|
key: "description",
|
||||||
label: "描述",
|
label: "描述",
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { App } from "antd";
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import {
|
import {
|
||||||
deleteDatasetFileUsingDelete,
|
deleteDatasetFileUsingDelete,
|
||||||
downloadFile,
|
downloadFileByIdUsingGet,
|
||||||
exportDatasetUsingPost,
|
exportDatasetUsingPost,
|
||||||
queryDatasetFilesUsingGet,
|
queryDatasetFilesUsingGet,
|
||||||
} from "../dataset.api";
|
} from "../dataset.api";
|
||||||
@@ -51,7 +51,7 @@ export function useFilesOperation(dataset: Dataset) {
|
|||||||
const handleDownloadFile = async (file: DatasetFile) => {
|
const handleDownloadFile = async (file: DatasetFile) => {
|
||||||
console.log("批量下载文件:", selectedFiles);
|
console.log("批量下载文件:", selectedFiles);
|
||||||
// 实际导出逻辑
|
// 实际导出逻辑
|
||||||
await downloadFile(dataset.id, file.id, file.fileName);
|
await downloadFileByIdUsingGet(dataset.id, file.id, file.fileName);
|
||||||
// 假设导出成功
|
// 假设导出成功
|
||||||
message.success({
|
message.success({
|
||||||
content: `已导出 1 个文件`,
|
content: `已导出 1 个文件`,
|
||||||
@@ -119,7 +119,13 @@ export default function DatasetManagementPage() {
|
|||||||
fetchData,
|
fetchData,
|
||||||
setSearchParams,
|
setSearchParams,
|
||||||
handleFiltersChange,
|
handleFiltersChange,
|
||||||
} = useFetchData(queryDatasetsUsingGet, mapDataset);
|
} = useFetchData<Dataset>(
|
||||||
|
queryDatasetsUsingGet,
|
||||||
|
mapDataset,
|
||||||
|
30000, // 30秒轮询间隔
|
||||||
|
true, // 自动刷新
|
||||||
|
[fetchStatistics] // 额外的轮询函数
|
||||||
|
);
|
||||||
|
|
||||||
const handleDownloadDataset = async (dataset: Dataset) => {
|
const handleDownloadDataset = async (dataset: Dataset) => {
|
||||||
await downloadDatasetUsingGet(dataset.id, dataset.name);
|
await downloadDatasetUsingGet(dataset.id, dataset.name);
|
||||||
@@ -138,9 +144,12 @@ export default function DatasetManagementPage() {
|
|||||||
setShowUploadDialog(true);
|
setShowUploadDialog(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
const handleRefresh = async (showMessage = true) => {
|
||||||
fetchStatistics();
|
await fetchData();
|
||||||
}, []);
|
if (showMessage) {
|
||||||
|
message.success("数据已刷新");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const operations = [
|
const operations = [
|
||||||
{
|
{
|
||||||
@@ -173,6 +182,13 @@ export default function DatasetManagementPage() {
|
|||||||
key: "delete",
|
key: "delete",
|
||||||
label: "删除",
|
label: "删除",
|
||||||
danger: true,
|
danger: true,
|
||||||
|
confirm: {
|
||||||
|
title: "确认删除该数据集?",
|
||||||
|
description: "删除后该数据集将无法恢复,请谨慎操作。",
|
||||||
|
okText: "删除",
|
||||||
|
cancelText: "取消",
|
||||||
|
okType: "danger",
|
||||||
|
},
|
||||||
icon: <DeleteOutlined />,
|
icon: <DeleteOutlined />,
|
||||||
onClick: (item: Dataset) => handleDeleteDataset(item.id),
|
onClick: (item: Dataset) => handleDeleteDataset(item.id),
|
||||||
},
|
},
|
||||||
@@ -306,7 +322,7 @@ export default function DatasetManagementPage() {
|
|||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h1 className="text-xl font-bold">数据管理</h1>
|
<h1 className="text-xl font-bold">数据管理</h1>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2 items-center">
|
||||||
{/* tasks */}
|
{/* tasks */}
|
||||||
<TagManager
|
<TagManager
|
||||||
onCreate={createDatasetTagUsingPost}
|
onCreate={createDatasetTagUsingPost}
|
||||||
@@ -351,7 +367,7 @@ export default function DatasetManagementPage() {
|
|||||||
viewMode={viewMode}
|
viewMode={viewMode}
|
||||||
onViewModeChange={setViewMode}
|
onViewModeChange={setViewMode}
|
||||||
showViewToggle
|
showViewToggle
|
||||||
onReload={fetchData}
|
onReload={handleRefresh}
|
||||||
/>
|
/>
|
||||||
{viewMode === "card" ? renderCardView() : renderListView()}
|
{viewMode === "card" ? renderCardView() : renderListView()}
|
||||||
<EditDataset
|
<EditDataset
|
||||||
@@ -361,7 +377,7 @@ export default function DatasetManagementPage() {
|
|||||||
setCurrentDataset(null);
|
setCurrentDataset(null);
|
||||||
setEditDatasetOpen(false);
|
setEditDatasetOpen(false);
|
||||||
}}
|
}}
|
||||||
onRefresh={fetchData}
|
onRefresh={handleRefresh}
|
||||||
/>
|
/>
|
||||||
<ImportConfiguration
|
<ImportConfiguration
|
||||||
data={currentDataset}
|
data={currentDataset}
|
||||||
@@ -370,7 +386,7 @@ export default function DatasetManagementPage() {
|
|||||||
setCurrentDataset(null);
|
setCurrentDataset(null);
|
||||||
setShowUploadDialog(false);
|
setShowUploadDialog(false);
|
||||||
}}
|
}}
|
||||||
onRefresh={fetchData}
|
onRefresh={handleRefresh}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { get, post, put, del, download, upload } from "@/utils/request";
|
import { get, post, put, del, download } from "@/utils/request";
|
||||||
|
|
||||||
// 数据集统计接口
|
// 数据集统计接口
|
||||||
export function getDatasetStatisticsUsingGet() {
|
export function getDatasetStatisticsUsingGet() {
|
||||||
@@ -35,15 +35,8 @@ export function deleteDatasetByIdUsingDelete(id: string | number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 下载数据集
|
// 下载数据集
|
||||||
export function downloadDatasetUsingGet(
|
export function downloadDatasetUsingGet(id: string | number) {
|
||||||
id: string | number,
|
return download(`/api/data-management/datasets/${id}/files/download`);
|
||||||
filename?: string
|
|
||||||
) {
|
|
||||||
return download(
|
|
||||||
`/api/data-management/datasets/${id}/download`,
|
|
||||||
null,
|
|
||||||
filename
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证数据集
|
// 验证数据集
|
||||||
@@ -61,8 +54,16 @@ export function uploadDatasetFileUsingPost(id: string | number, data: any) {
|
|||||||
return post(`/api/data-management/datasets/${id}/files`, data);
|
return post(`/api/data-management/datasets/${id}/files`, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function downloadFile(id: string | number) {
|
export function downloadFileByIdUsingGet(
|
||||||
return download(`/api/data-management/datasets/${id}/files/download`);
|
id: string | number,
|
||||||
|
fileId: string | number,
|
||||||
|
fileName: string
|
||||||
|
) {
|
||||||
|
return download(
|
||||||
|
`/api/data-management/datasets/${id}/files/${fileId}/download`,
|
||||||
|
null,
|
||||||
|
fileName
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 删除数据集文件
|
// 删除数据集文件
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
CloseCircleOutlined,
|
CloseCircleOutlined,
|
||||||
FileOutlined,
|
FileOutlined,
|
||||||
} from "@ant-design/icons";
|
} from "@ant-design/icons";
|
||||||
|
import { AnyObject } from "antd/es/_util/type";
|
||||||
import {
|
import {
|
||||||
FileImage,
|
FileImage,
|
||||||
FileText,
|
FileText,
|
||||||
@@ -194,7 +195,7 @@ export const dataSourceMap: Record<string, { label: string; value: string }> = {
|
|||||||
|
|
||||||
export const dataSourceOptions = Object.values(dataSourceMap);
|
export const dataSourceOptions = Object.values(dataSourceMap);
|
||||||
|
|
||||||
export function mapDataset(dataset: Dataset) {
|
export function mapDataset(dataset: AnyObject): Dataset {
|
||||||
const { icon: IconComponent, iconColor } =
|
const { icon: IconComponent, iconColor } =
|
||||||
datasetTypeMap[dataset?.datasetType] || {};
|
datasetTypeMap[dataset?.datasetType] || {};
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
export { useFilesOperation } from "./useFilesOperation";
|
|
||||||
export { useImportFile } from "./useImportFile";
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
import { Upload, type UploadFile } from "antd";
|
|
||||||
import { InboxOutlined } from "@ant-design/icons";
|
|
||||||
import { useMemo, useState } from "react";
|
|
||||||
import type { Dataset } from "@/pages/DataManagement/dataset.model";
|
|
||||||
import { sliceFile } from "@/utils/file.util";
|
|
||||||
|
|
||||||
const { Dragger } = Upload;
|
|
||||||
|
|
||||||
export const useImportFile = () => {
|
|
||||||
const [fileList, setFileList] = useState<UploadFile[]>([]);
|
|
||||||
const fileSliceList = useMemo(() => {
|
|
||||||
const sliceList = fileList.map((file) => {
|
|
||||||
const slices = sliceFile(file);
|
|
||||||
return { originFile: file, slices, name: file.name, size: file.size };
|
|
||||||
});
|
|
||||||
return sliceList;
|
|
||||||
}, [fileList]);
|
|
||||||
|
|
||||||
const resetFiles = () => {
|
|
||||||
setFileList([]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleUpload = async (dataset: Dataset) => {
|
|
||||||
const formData = new FormData();
|
|
||||||
fileList.forEach((file) => {
|
|
||||||
formData.append("file", file);
|
|
||||||
});
|
|
||||||
window.dispatchEvent(
|
|
||||||
new CustomEvent("upload:dataset", {
|
|
||||||
detail: { dataset, files: fileSliceList },
|
|
||||||
})
|
|
||||||
);
|
|
||||||
resetFiles();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleBeforeUpload = (_, files: UploadFile[]) => {
|
|
||||||
setFileList([...fileList, ...files]);
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRemoveFile = (file: UploadFile) => {
|
|
||||||
setFileList((prev) => prev.filter((f) => f.uid !== file.uid));
|
|
||||||
};
|
|
||||||
|
|
||||||
const importFileRender = () => (
|
|
||||||
<Dragger
|
|
||||||
className="w-full"
|
|
||||||
onRemove={handleRemoveFile}
|
|
||||||
beforeUpload={handleBeforeUpload}
|
|
||||||
multiple
|
|
||||||
>
|
|
||||||
<p className="ant-upload-drag-icon">
|
|
||||||
<InboxOutlined />
|
|
||||||
</p>
|
|
||||||
<p className="ant-upload-text">本地文件上传</p>
|
|
||||||
<p className="ant-upload-hint">拖拽文件到此处或点击选择文件</p>
|
|
||||||
</Dragger>
|
|
||||||
);
|
|
||||||
|
|
||||||
return { fileList, resetFiles, handleUpload, importFileRender };
|
|
||||||
};
|
|
||||||
@@ -71,7 +71,7 @@ const AsiderAndHeaderLayout = () => {
|
|||||||
<div className="w-8 h-8 bg-gradient-to-br from-blue-500 to-indigo-600 rounded-lg flex items-center justify-center">
|
<div className="w-8 h-8 bg-gradient-to-br from-blue-500 to-indigo-600 rounded-lg flex items-center justify-center">
|
||||||
<Sparkles className="w-5 h-5 text-white" />
|
<Sparkles className="w-5 h-5 text-white" />
|
||||||
</div>
|
</div>
|
||||||
<span className="text-lg font-bold text-gray-900">ModelEngine</span>
|
<span className="text-lg font-bold text-gray-900">DataMate</span>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
)}
|
)}
|
||||||
<span
|
<span
|
||||||
|
|||||||
@@ -326,7 +326,6 @@ class Request {
|
|||||||
...options,
|
...options,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
console.log("post", url, config);
|
|
||||||
return this.request(this.baseURL + url, config);
|
return this.request(this.baseURL + url, config);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -403,7 +402,7 @@ class Request {
|
|||||||
* @param {string} filename - 下载文件名
|
* @param {string} filename - 下载文件名
|
||||||
* @param {object} options - 额外的fetch选项,包括showLoading, onDownloadProgress
|
* @param {object} options - 额外的fetch选项,包括showLoading, onDownloadProgress
|
||||||
*/
|
*/
|
||||||
async download(url, params = null, filename = "download", options = {}) {
|
async download(url, params = null, filename = "", options = {}) {
|
||||||
const fullURL = this.buildURL(url, params);
|
const fullURL = this.buildURL(url, params);
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
@@ -416,6 +415,7 @@ class Request {
|
|||||||
const processedConfig = await this.executeRequestInterceptors(config);
|
const processedConfig = await this.executeRequestInterceptors(config);
|
||||||
|
|
||||||
let blob;
|
let blob;
|
||||||
|
let name = filename;
|
||||||
|
|
||||||
// 如果需要下载进度监听,使用XMLHttpRequest
|
// 如果需要下载进度监听,使用XMLHttpRequest
|
||||||
if (config.onDownloadProgress) {
|
if (config.onDownloadProgress) {
|
||||||
@@ -431,6 +431,10 @@ class Request {
|
|||||||
}
|
}
|
||||||
|
|
||||||
blob = xhrResponse.xhr.response;
|
blob = xhrResponse.xhr.response;
|
||||||
|
name =
|
||||||
|
name ||
|
||||||
|
xhrResponse.headers.get("Content-Disposition")?.split("filename=")[1] ||
|
||||||
|
"download";
|
||||||
} else {
|
} else {
|
||||||
// 使用fetch
|
// 使用fetch
|
||||||
const response = await fetch(fullURL, processedConfig);
|
const response = await fetch(fullURL, processedConfig);
|
||||||
@@ -446,13 +450,17 @@ class Request {
|
|||||||
}
|
}
|
||||||
|
|
||||||
blob = await processedResponse.blob();
|
blob = await processedResponse.blob();
|
||||||
|
name =
|
||||||
|
name ||
|
||||||
|
response.headers.get("Content-Disposition")?.split("filename=")[1] ||
|
||||||
|
`download_${Date.now()}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建下载链接
|
// 创建下载链接
|
||||||
const downloadUrl = window.URL.createObjectURL(blob);
|
const downloadUrl = window.URL.createObjectURL(blob);
|
||||||
const link = document.createElement("a");
|
const link = document.createElement("a");
|
||||||
link.href = downloadUrl;
|
link.href = downloadUrl;
|
||||||
link.download = filename;
|
link.download = filename ?? name;
|
||||||
|
|
||||||
// 添加到DOM并触发下载
|
// 添加到DOM并触发下载
|
||||||
document.body.appendChild(link);
|
document.body.appendChild(link);
|
||||||
|
|||||||
Reference in New Issue
Block a user