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:
chenghh-9609
2025-10-23 15:37:22 +08:00
parent a6d4b51601
commit bb116839ae
19 changed files with 397 additions and 1007 deletions

View File

@@ -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%;
}
}

View File

@@ -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;