You've already forked DataMate
Revert "feat: fix the problem in the Operator Market frontend pages"
This commit is contained in:
@@ -1,480 +1,480 @@
|
||||
import type React from "react";
|
||||
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import { Card, Input, Button, Badge } from "antd";
|
||||
import { HomeOutlined } from "@ant-design/icons";
|
||||
import {
|
||||
MessageSquare,
|
||||
Send,
|
||||
Bot,
|
||||
User,
|
||||
Sparkles,
|
||||
Database,
|
||||
BarChart3,
|
||||
Settings,
|
||||
Zap,
|
||||
CheckCircle,
|
||||
Clock,
|
||||
Download,
|
||||
ArrowLeft,
|
||||
} from "lucide-react";
|
||||
import { useNavigate } from "react-router";
|
||||
import DevelopmentInProgress from "@/components/DevelopmentInProgress";
|
||||
|
||||
interface Message {
|
||||
id: string;
|
||||
type: "user" | "assistant";
|
||||
content: string;
|
||||
timestamp: Date;
|
||||
actions?: Array<{
|
||||
type:
|
||||
| "create_dataset"
|
||||
| "run_analysis"
|
||||
| "start_synthesis"
|
||||
| "export_report";
|
||||
label: string;
|
||||
data?: any;
|
||||
}>;
|
||||
status?: "pending" | "completed" | "error";
|
||||
}
|
||||
|
||||
interface QuickAction {
|
||||
id: string;
|
||||
label: string;
|
||||
icon: any;
|
||||
prompt: string;
|
||||
category: string;
|
||||
}
|
||||
|
||||
const quickActions: QuickAction[] = [
|
||||
{
|
||||
id: "create_dataset",
|
||||
label: "创建数据集",
|
||||
icon: Database,
|
||||
prompt: "帮我创建一个新的数据集",
|
||||
category: "数据管理",
|
||||
},
|
||||
{
|
||||
id: "analyze_quality",
|
||||
label: "质量分析",
|
||||
icon: BarChart3,
|
||||
prompt: "分析我的数据集质量",
|
||||
category: "数据评估",
|
||||
},
|
||||
{
|
||||
id: "start_synthesis",
|
||||
label: "数据合成",
|
||||
icon: Sparkles,
|
||||
prompt: "启动数据合成任务",
|
||||
category: "数据合成",
|
||||
},
|
||||
{
|
||||
id: "process_data",
|
||||
label: "数据清洗",
|
||||
icon: Settings,
|
||||
prompt: "对数据集进行预处理",
|
||||
category: "数据清洗",
|
||||
},
|
||||
{
|
||||
id: "export_report",
|
||||
label: "导出报告",
|
||||
icon: Download,
|
||||
prompt: "导出最新的分析报告",
|
||||
category: "报告导出",
|
||||
},
|
||||
{
|
||||
id: "check_status",
|
||||
label: "查看状态",
|
||||
icon: Clock,
|
||||
prompt: "查看所有任务的运行状态",
|
||||
category: "状态查询",
|
||||
},
|
||||
];
|
||||
|
||||
const mockResponses = {
|
||||
创建数据集: {
|
||||
content:
|
||||
"我来帮您创建一个新的数据集。请告诉我以下信息:\n\n1. 数据集名称\n2. 数据类型(图像、文本、问答对等)\n3. 预期数据量\n4. 数据来源\n\n您也可以直接说出您的需求,我会为您推荐最适合的配置。",
|
||||
actions: [
|
||||
{ type: "create_dataset", label: "开始创建", data: { step: "config" } },
|
||||
],
|
||||
},
|
||||
质量分析: {
|
||||
content:
|
||||
"正在为您分析数据集质量...\n\n📊 **分析结果概览:**\n- 图像分类数据集:质量分 92/100\n- 问答对数据集:质量分 87/100\n- 多模态数据集:质量分 78/100\n\n🔍 **发现的主要问题:**\n- 23个重复图像\n- 156个格式不正确的问答对\n- 78个图文不匹配项\n\n💡 **改进建议:**\n- 建议进行去重处理\n- 优化问答对格式\n- 重新标注图文匹配项",
|
||||
actions: [
|
||||
{
|
||||
type: "run_analysis",
|
||||
label: "查看详细报告",
|
||||
data: { type: "detailed" },
|
||||
},
|
||||
],
|
||||
},
|
||||
数据合成: {
|
||||
content:
|
||||
"我可以帮您启动数据合成任务。目前支持以下合成类型:\n\n🖼️ **图像数据合成**\n- 数据增强(旋转、翻转、亮度调整)\n- 风格迁移\n- GAN生成\n\n📝 **文本数据合成**\n- 同义词替换\n- 回译增强\n- GPT生成\n\n❓ **问答对合成**\n- 基于知识库生成\n- 模板变换\n- 多轮对话生成\n\n请告诉我您需要合成什么类型的数据,以及目标数量。",
|
||||
actions: [
|
||||
{
|
||||
type: "start_synthesis",
|
||||
label: "配置合成任务",
|
||||
data: { step: "config" },
|
||||
},
|
||||
],
|
||||
},
|
||||
导出报告: {
|
||||
content:
|
||||
"正在为您准备最新的分析报告...\n\n📋 **可用报告:**\n- 数据质量评估报告(PDF)\n- 数据分布统计报告(Excel)\n- 模型性能评估报告(PDF)\n- 偏见检测报告(PDF)\n- 综合分析报告(PDF + Excel)\n\n✅ 报告已生成完成,您可以选择下载格式。",
|
||||
actions: [
|
||||
{ type: "export_report", label: "下载报告", data: { format: "pdf" } },
|
||||
],
|
||||
},
|
||||
查看状态: {
|
||||
content:
|
||||
"📊 **当前任务状态概览:**\n\n🟢 **运行中的任务:**\n- 问答对生成任务:65% 完成\n- 图像质量分析:运行中\n- 知识库构建:等待中\n\n✅ **已完成的任务:**\n- 图像分类数据集创建:已完成\n- PDF文档提取:已完成\n- 训练集配比任务:已完成\n\n⚠️ **需要关注的任务:**\n- 多模态数据合成:暂停(需要用户确认参数)\n\n所有任务运行正常,预计2小时内全部完成。",
|
||||
actions: [],
|
||||
},
|
||||
};
|
||||
|
||||
export default function AgentPage() {
|
||||
return <DevelopmentInProgress />;
|
||||
const navigate = useNavigate();
|
||||
const [messages, setMessages] = useState<Message[]>([
|
||||
{
|
||||
id: "welcome",
|
||||
type: "assistant",
|
||||
content:
|
||||
"👋 您好!我是 Data Agent,您的AI数据助手。\n\n我可以帮您:\n• 创建和管理数据集\n• 分析数据质量\n• 启动处理任务\n• 生成分析报告\n• 回答数据相关问题\n\n请告诉我您需要什么帮助,或者点击下方的快捷操作开始。",
|
||||
timestamp: new Date(),
|
||||
},
|
||||
]);
|
||||
const [inputValue, setInputValue] = useState("");
|
||||
const [isTyping, setIsTyping] = useState(false);
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
const inputRef = useRef<any>(null);
|
||||
|
||||
const scrollToBottom = () => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
scrollToBottom();
|
||||
}, [messages]);
|
||||
|
||||
const handleSendMessage = async (content: string) => {
|
||||
if (!content.trim()) return;
|
||||
|
||||
const userMessage: Message = {
|
||||
id: Date.now().toString(),
|
||||
type: "user",
|
||||
content: content.trim(),
|
||||
timestamp: new Date(),
|
||||
};
|
||||
|
||||
setMessages((prev) => [...prev, userMessage]);
|
||||
setInputValue("");
|
||||
setIsTyping(true);
|
||||
|
||||
// 模拟AI响应
|
||||
setTimeout(() => {
|
||||
const response = generateResponse(content);
|
||||
const assistantMessage: Message = {
|
||||
id: (Date.now() + 1).toString(),
|
||||
type: "assistant",
|
||||
content: response.content,
|
||||
timestamp: new Date(),
|
||||
actions: response.actions,
|
||||
};
|
||||
|
||||
setMessages((prev) => [...prev, assistantMessage]);
|
||||
setIsTyping(false);
|
||||
}, 1500);
|
||||
};
|
||||
|
||||
const generateResponse = (
|
||||
input: string
|
||||
): { content: string; actions?: any[] } => {
|
||||
const lowerInput = input.toLowerCase();
|
||||
|
||||
if (lowerInput.includes("创建") && lowerInput.includes("数据集")) {
|
||||
return mockResponses["创建数据集"];
|
||||
} else if (lowerInput.includes("质量") || lowerInput.includes("分析")) {
|
||||
return mockResponses["质量分析"];
|
||||
} else if (lowerInput.includes("合成") || lowerInput.includes("生成")) {
|
||||
return mockResponses["数据合成"];
|
||||
} else if (lowerInput.includes("导出") || lowerInput.includes("报告")) {
|
||||
return mockResponses["导出报告"];
|
||||
} else if (lowerInput.includes("状态") || lowerInput.includes("任务")) {
|
||||
return mockResponses["查看状态"];
|
||||
} else if (lowerInput.includes("你好") || lowerInput.includes("帮助")) {
|
||||
return {
|
||||
content:
|
||||
"很高兴为您服务!我是专门为数据集管理设计的AI助手。\n\n我的主要能力包括:\n\n🔧 **数据集操作**\n- 创建、导入、导出数据集\n- 数据预处理和清洗\n- 批量操作和自动化\n\n📊 **智能分析**\n- 数据质量评估\n- 分布统计分析\n- 性能和偏见检测\n\n🤖 **AI增强**\n- 智能数据合成\n- 自动标注建议\n- 知识库构建\n\n请告诉我您的具体需求,我会为您提供最合适的解决方案!",
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
content: `我理解您想要「${input}」。让我为您分析一下...\n\n基于您的需求,我建议:\n\n1. 首先确认具体的操作目标\n2. 选择合适的数据集和参数\n3. 执行相应的处理流程\n\n您可以提供更多详细信息,或者选择下方的快捷操作来开始。如果需要帮助,请说"帮助"获取完整功能列表。`,
|
||||
actions: [
|
||||
{ type: "run_analysis", label: "开始分析", data: { query: input } },
|
||||
],
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const handleQuickAction = (action: QuickAction) => {
|
||||
handleSendMessage(action.prompt);
|
||||
};
|
||||
|
||||
const handleActionClick = (action: any) => {
|
||||
const actionMessage: Message = {
|
||||
id: Date.now().toString(),
|
||||
type: "assistant",
|
||||
content: `✅ 正在执行「${action.label}」...\n\n操作已启动,您可以在相应的功能模块中查看详细进度。`,
|
||||
timestamp: new Date(),
|
||||
status: "completed",
|
||||
};
|
||||
setMessages((prev) => [...prev, actionMessage]);
|
||||
};
|
||||
|
||||
const handleKeyPress = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === "Enter" && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
handleSendMessage(inputValue);
|
||||
}
|
||||
};
|
||||
|
||||
const formatMessage = (content: string) => {
|
||||
return content.split("\n").map((line, index) => (
|
||||
<div key={index} className="mb-1">
|
||||
{line || <br />}
|
||||
</div>
|
||||
));
|
||||
};
|
||||
|
||||
const onBack = () => {
|
||||
navigate("/");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-purple-50 to-pink-50">
|
||||
<div className="h-screen flex flex-col">
|
||||
{/* Header */}
|
||||
<div className="bg-gradient-to-r from-purple-500 to-pink-500 text-white p-6">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-12 h-12 bg-white/20 rounded-lg flex items-center justify-center">
|
||||
<MessageSquare className="w-6 h-6" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">Data Agent</h1>
|
||||
<p className="text-purple-100">
|
||||
AI驱动的智能数据助手,通过对话完成复杂数据操作
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
type="default"
|
||||
icon={<ArrowLeft className="w-4 h-4 mr-2" />}
|
||||
onClick={onBack}
|
||||
className="bg-white/10 border-white/20 text-white hover:bg-white/20 hover:border-white/30"
|
||||
>
|
||||
返回首页
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 max-w-7xl mx-auto h-full w-full p-6">
|
||||
<div className="h-full flex gap-6">
|
||||
{/* Chat Area */}
|
||||
<div className="lg:col-span-3 flex flex-1 flex-col h-full">
|
||||
<div className="flex-1 flex flex-col h-full shadow-lg">
|
||||
<div className="pb-3 bg-white rounded-t-lg">
|
||||
<div className="flex items-center justify-between p-4">
|
||||
<span className="text-lg font-semibold">对话窗口</span>
|
||||
<div>
|
||||
<span className="w-2 h-2 bg-green-500 rounded-full mr-1 inline-block" />
|
||||
在线
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 flex flex-col justify-between h-full p-0 min-h-0">
|
||||
{/* Messages */}
|
||||
<div className="flex-1 overflow-y-auto p-6 bg-white">
|
||||
<div className="space-y-4 pb-4">
|
||||
{messages.map((message) => (
|
||||
<div
|
||||
key={message.id}
|
||||
className={`flex gap-3 ${
|
||||
message.type === "user"
|
||||
? "justify-end"
|
||||
: "justify-start"
|
||||
}`}
|
||||
>
|
||||
{message.type === "assistant" && (
|
||||
<div className="w-8 h-8 bg-gradient-to-br from-purple-500 to-pink-500 rounded-full flex items-center justify-center flex-shrink-0">
|
||||
<Bot className="w-4 h-4 text-white" />
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={`max-w-[80%] rounded-lg px-4 py-3 ${
|
||||
message.type === "user"
|
||||
? "bg-blue-500 text-white"
|
||||
: "bg-white text-gray-900 shadow-sm border border-gray-100"
|
||||
}`}
|
||||
>
|
||||
<div className="text-sm whitespace-pre-wrap">
|
||||
{formatMessage(message.content)}
|
||||
</div>
|
||||
{message.actions && message.actions.length > 0 && (
|
||||
<div className="mt-3 space-y-2">
|
||||
{message.actions.map((action, index) => (
|
||||
<Button
|
||||
key={index}
|
||||
type="default"
|
||||
size="small"
|
||||
className="mr-2 mb-2"
|
||||
onClick={() => handleActionClick(action)}
|
||||
>
|
||||
{action.label}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className="text-xs opacity-70 mt-2">
|
||||
{message.timestamp.toLocaleTimeString()}
|
||||
</div>
|
||||
</div>
|
||||
{message.type === "user" && (
|
||||
<div className="w-8 h-8 bg-blue-500 rounded-full flex items-center justify-center flex-shrink-0">
|
||||
<User className="w-4 h-4 text-white" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{isTyping && (
|
||||
<div className="flex gap-3 justify-start">
|
||||
<div className="w-8 h-8 bg-gradient-to-br from-purple-500 to-pink-500 rounded-full flex items-center justify-center">
|
||||
<Bot className="w-4 h-4 text-white" />
|
||||
</div>
|
||||
<div className="bg-white rounded-lg px-4 py-3 shadow-sm border border-gray-100">
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce"></div>
|
||||
<div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce delay-100"></div>
|
||||
<div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce delay-200"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
|
||||
{/* Input Area */}
|
||||
<div className="border-t border-gray-200 p-4 bg-white rounded-b-lg">
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
ref={inputRef}
|
||||
value={inputValue}
|
||||
onChange={(e) => setInputValue(e.target.value)}
|
||||
onKeyDown={handleKeyPress}
|
||||
placeholder="输入您的需求,例如:创建一个图像分类数据集..."
|
||||
disabled={isTyping}
|
||||
/>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => handleSendMessage(inputValue)}
|
||||
disabled={!inputValue.trim() || isTyping}
|
||||
className="bg-gradient-to-r from-purple-400 to-pink-400 border-none hover:from-purple-500 hover:to-pink-500"
|
||||
>
|
||||
<Send className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Quick Actions Sidebar */}
|
||||
<div className="w-72 flex flex-col gap-6">
|
||||
<Card className="shadow-lg">
|
||||
<div className="">
|
||||
<span className="text-lg font-semibold">快捷操作</span>
|
||||
<div className="text-sm text-gray-500">
|
||||
点击快速开始常用操作
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2 p-4">
|
||||
{quickActions.map((action) => (
|
||||
<Button
|
||||
key={action.id}
|
||||
type="default"
|
||||
className="w-full justify-start h-auto p-3 text-left"
|
||||
onClick={() => handleQuickAction(action)}
|
||||
>
|
||||
<action.icon className="w-4 h-4 mr-2 flex-shrink-0" />
|
||||
<div className="text-left">
|
||||
<div className="font-medium text-sm">
|
||||
{action.label}
|
||||
</div>
|
||||
</div>
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="shadow-lg">
|
||||
<div className="pb-3">
|
||||
<span className="text-lg font-semibold">系统状态</span>
|
||||
</div>
|
||||
<div className="space-y-3 p-4 pt-0">
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<CheckCircle className="w-4 h-4 text-green-500" />
|
||||
<span>AI服务正常</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<Clock className="w-4 h-4 text-blue-500" />
|
||||
<span>3个任务运行中</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<Database className="w-4 h-4 text-purple-500" />
|
||||
<span>12个数据集就绪</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<Zap className="w-4 h-4 text-orange-500" />
|
||||
<span>响应时间: 0.8s</span>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="shadow-lg">
|
||||
<div className="pb-3">
|
||||
<span className="text-lg font-semibold">使用提示</span>
|
||||
</div>
|
||||
<div className="space-y-2 text-sm text-gray-600 p-4 pt-0">
|
||||
<div>💡 您可以用自然语言描述需求</div>
|
||||
<div>🔍 支持复杂的多步骤操作</div>
|
||||
<div>📊 可以询问数据统计和分析</div>
|
||||
<div>⚡ 使用快捷操作提高效率</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="shadow-lg">
|
||||
<div className="pt-6 p-4">
|
||||
<Button
|
||||
type="default"
|
||||
className="w-full"
|
||||
icon={<HomeOutlined />}
|
||||
onClick={onBack}
|
||||
>
|
||||
返回主应用
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
import type React from "react";
|
||||
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import { Card, Input, Button, Badge } from "antd";
|
||||
import { HomeOutlined } from "@ant-design/icons";
|
||||
import {
|
||||
MessageSquare,
|
||||
Send,
|
||||
Bot,
|
||||
User,
|
||||
Sparkles,
|
||||
Database,
|
||||
BarChart3,
|
||||
Settings,
|
||||
Zap,
|
||||
CheckCircle,
|
||||
Clock,
|
||||
Download,
|
||||
ArrowLeft,
|
||||
} from "lucide-react";
|
||||
import { useNavigate } from "react-router";
|
||||
import DevelopmentInProgress from "@/components/DevelopmentInProgress";
|
||||
|
||||
interface Message {
|
||||
id: string;
|
||||
type: "user" | "assistant";
|
||||
content: string;
|
||||
timestamp: Date;
|
||||
actions?: Array<{
|
||||
type:
|
||||
| "create_dataset"
|
||||
| "run_analysis"
|
||||
| "start_synthesis"
|
||||
| "export_report";
|
||||
label: string;
|
||||
data?: any;
|
||||
}>;
|
||||
status?: "pending" | "completed" | "error";
|
||||
}
|
||||
|
||||
interface QuickAction {
|
||||
id: string;
|
||||
label: string;
|
||||
icon: any;
|
||||
prompt: string;
|
||||
category: string;
|
||||
}
|
||||
|
||||
const quickActions: QuickAction[] = [
|
||||
{
|
||||
id: "create_dataset",
|
||||
label: "创建数据集",
|
||||
icon: Database,
|
||||
prompt: "帮我创建一个新的数据集",
|
||||
category: "数据管理",
|
||||
},
|
||||
{
|
||||
id: "analyze_quality",
|
||||
label: "质量分析",
|
||||
icon: BarChart3,
|
||||
prompt: "分析我的数据集质量",
|
||||
category: "数据评估",
|
||||
},
|
||||
{
|
||||
id: "start_synthesis",
|
||||
label: "数据合成",
|
||||
icon: Sparkles,
|
||||
prompt: "启动数据合成任务",
|
||||
category: "数据合成",
|
||||
},
|
||||
{
|
||||
id: "process_data",
|
||||
label: "数据清洗",
|
||||
icon: Settings,
|
||||
prompt: "对数据集进行预处理",
|
||||
category: "数据清洗",
|
||||
},
|
||||
{
|
||||
id: "export_report",
|
||||
label: "导出报告",
|
||||
icon: Download,
|
||||
prompt: "导出最新的分析报告",
|
||||
category: "报告导出",
|
||||
},
|
||||
{
|
||||
id: "check_status",
|
||||
label: "查看状态",
|
||||
icon: Clock,
|
||||
prompt: "查看所有任务的运行状态",
|
||||
category: "状态查询",
|
||||
},
|
||||
];
|
||||
|
||||
const mockResponses = {
|
||||
创建数据集: {
|
||||
content:
|
||||
"我来帮您创建一个新的数据集。请告诉我以下信息:\n\n1. 数据集名称\n2. 数据类型(图像、文本、问答对等)\n3. 预期数据量\n4. 数据来源\n\n您也可以直接说出您的需求,我会为您推荐最适合的配置。",
|
||||
actions: [
|
||||
{ type: "create_dataset", label: "开始创建", data: { step: "config" } },
|
||||
],
|
||||
},
|
||||
质量分析: {
|
||||
content:
|
||||
"正在为您分析数据集质量...\n\n📊 **分析结果概览:**\n- 图像分类数据集:质量分 92/100\n- 问答对数据集:质量分 87/100\n- 多模态数据集:质量分 78/100\n\n🔍 **发现的主要问题:**\n- 23个重复图像\n- 156个格式不正确的问答对\n- 78个图文不匹配项\n\n💡 **改进建议:**\n- 建议进行去重处理\n- 优化问答对格式\n- 重新标注图文匹配项",
|
||||
actions: [
|
||||
{
|
||||
type: "run_analysis",
|
||||
label: "查看详细报告",
|
||||
data: { type: "detailed" },
|
||||
},
|
||||
],
|
||||
},
|
||||
数据合成: {
|
||||
content:
|
||||
"我可以帮您启动数据合成任务。目前支持以下合成类型:\n\n🖼️ **图像数据合成**\n- 数据增强(旋转、翻转、亮度调整)\n- 风格迁移\n- GAN生成\n\n📝 **文本数据合成**\n- 同义词替换\n- 回译增强\n- GPT生成\n\n❓ **问答对合成**\n- 基于知识库生成\n- 模板变换\n- 多轮对话生成\n\n请告诉我您需要合成什么类型的数据,以及目标数量。",
|
||||
actions: [
|
||||
{
|
||||
type: "start_synthesis",
|
||||
label: "配置合成任务",
|
||||
data: { step: "config" },
|
||||
},
|
||||
],
|
||||
},
|
||||
导出报告: {
|
||||
content:
|
||||
"正在为您准备最新的分析报告...\n\n📋 **可用报告:**\n- 数据质量评估报告(PDF)\n- 数据分布统计报告(Excel)\n- 模型性能评估报告(PDF)\n- 偏见检测报告(PDF)\n- 综合分析报告(PDF + Excel)\n\n✅ 报告已生成完成,您可以选择下载格式。",
|
||||
actions: [
|
||||
{ type: "export_report", label: "下载报告", data: { format: "pdf" } },
|
||||
],
|
||||
},
|
||||
查看状态: {
|
||||
content:
|
||||
"📊 **当前任务状态概览:**\n\n🟢 **运行中的任务:**\n- 问答对生成任务:65% 完成\n- 图像质量分析:运行中\n- 知识库构建:等待中\n\n✅ **已完成的任务:**\n- 图像分类数据集创建:已完成\n- PDF文档提取:已完成\n- 训练集配比任务:已完成\n\n⚠️ **需要关注的任务:**\n- 多模态数据合成:暂停(需要用户确认参数)\n\n所有任务运行正常,预计2小时内全部完成。",
|
||||
actions: [],
|
||||
},
|
||||
};
|
||||
|
||||
export default function AgentPage() {
|
||||
return <DevelopmentInProgress />;
|
||||
const navigate = useNavigate();
|
||||
const [messages, setMessages] = useState<Message[]>([
|
||||
{
|
||||
id: "welcome",
|
||||
type: "assistant",
|
||||
content:
|
||||
"👋 您好!我是 Data Agent,您的AI数据助手。\n\n我可以帮您:\n• 创建和管理数据集\n• 分析数据质量\n• 启动处理任务\n• 生成分析报告\n• 回答数据相关问题\n\n请告诉我您需要什么帮助,或者点击下方的快捷操作开始。",
|
||||
timestamp: new Date(),
|
||||
},
|
||||
]);
|
||||
const [inputValue, setInputValue] = useState("");
|
||||
const [isTyping, setIsTyping] = useState(false);
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
const inputRef = useRef<any>(null);
|
||||
|
||||
const scrollToBottom = () => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
scrollToBottom();
|
||||
}, [messages]);
|
||||
|
||||
const handleSendMessage = async (content: string) => {
|
||||
if (!content.trim()) return;
|
||||
|
||||
const userMessage: Message = {
|
||||
id: Date.now().toString(),
|
||||
type: "user",
|
||||
content: content.trim(),
|
||||
timestamp: new Date(),
|
||||
};
|
||||
|
||||
setMessages((prev) => [...prev, userMessage]);
|
||||
setInputValue("");
|
||||
setIsTyping(true);
|
||||
|
||||
// 模拟AI响应
|
||||
setTimeout(() => {
|
||||
const response = generateResponse(content);
|
||||
const assistantMessage: Message = {
|
||||
id: (Date.now() + 1).toString(),
|
||||
type: "assistant",
|
||||
content: response.content,
|
||||
timestamp: new Date(),
|
||||
actions: response.actions,
|
||||
};
|
||||
|
||||
setMessages((prev) => [...prev, assistantMessage]);
|
||||
setIsTyping(false);
|
||||
}, 1500);
|
||||
};
|
||||
|
||||
const generateResponse = (
|
||||
input: string
|
||||
): { content: string; actions?: any[] } => {
|
||||
const lowerInput = input.toLowerCase();
|
||||
|
||||
if (lowerInput.includes("创建") && lowerInput.includes("数据集")) {
|
||||
return mockResponses["创建数据集"];
|
||||
} else if (lowerInput.includes("质量") || lowerInput.includes("分析")) {
|
||||
return mockResponses["质量分析"];
|
||||
} else if (lowerInput.includes("合成") || lowerInput.includes("生成")) {
|
||||
return mockResponses["数据合成"];
|
||||
} else if (lowerInput.includes("导出") || lowerInput.includes("报告")) {
|
||||
return mockResponses["导出报告"];
|
||||
} else if (lowerInput.includes("状态") || lowerInput.includes("任务")) {
|
||||
return mockResponses["查看状态"];
|
||||
} else if (lowerInput.includes("你好") || lowerInput.includes("帮助")) {
|
||||
return {
|
||||
content:
|
||||
"很高兴为您服务!我是专门为数据集管理设计的AI助手。\n\n我的主要能力包括:\n\n🔧 **数据集操作**\n- 创建、导入、导出数据集\n- 数据预处理和清洗\n- 批量操作和自动化\n\n📊 **智能分析**\n- 数据质量评估\n- 分布统计分析\n- 性能和偏见检测\n\n🤖 **AI增强**\n- 智能数据合成\n- 自动标注建议\n- 知识库构建\n\n请告诉我您的具体需求,我会为您提供最合适的解决方案!",
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
content: `我理解您想要「${input}」。让我为您分析一下...\n\n基于您的需求,我建议:\n\n1. 首先确认具体的操作目标\n2. 选择合适的数据集和参数\n3. 执行相应的处理流程\n\n您可以提供更多详细信息,或者选择下方的快捷操作来开始。如果需要帮助,请说"帮助"获取完整功能列表。`,
|
||||
actions: [
|
||||
{ type: "run_analysis", label: "开始分析", data: { query: input } },
|
||||
],
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const handleQuickAction = (action: QuickAction) => {
|
||||
handleSendMessage(action.prompt);
|
||||
};
|
||||
|
||||
const handleActionClick = (action: any) => {
|
||||
const actionMessage: Message = {
|
||||
id: Date.now().toString(),
|
||||
type: "assistant",
|
||||
content: `✅ 正在执行「${action.label}」...\n\n操作已启动,您可以在相应的功能模块中查看详细进度。`,
|
||||
timestamp: new Date(),
|
||||
status: "completed",
|
||||
};
|
||||
setMessages((prev) => [...prev, actionMessage]);
|
||||
};
|
||||
|
||||
const handleKeyPress = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === "Enter" && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
handleSendMessage(inputValue);
|
||||
}
|
||||
};
|
||||
|
||||
const formatMessage = (content: string) => {
|
||||
return content.split("\n").map((line, index) => (
|
||||
<div key={index} className="mb-1">
|
||||
{line || <br />}
|
||||
</div>
|
||||
));
|
||||
};
|
||||
|
||||
const onBack = () => {
|
||||
navigate("/");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-purple-50 to-pink-50">
|
||||
<div className="h-screen flex flex-col">
|
||||
{/* Header */}
|
||||
<div className="bg-gradient-to-r from-purple-500 to-pink-500 text-white p-6">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-12 h-12 bg-white/20 rounded-lg flex items-center justify-center">
|
||||
<MessageSquare className="w-6 h-6" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">Data Agent</h1>
|
||||
<p className="text-purple-100">
|
||||
AI驱动的智能数据助手,通过对话完成复杂数据操作
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
type="default"
|
||||
icon={<ArrowLeft className="w-4 h-4 mr-2" />}
|
||||
onClick={onBack}
|
||||
className="bg-white/10 border-white/20 text-white hover:bg-white/20 hover:border-white/30"
|
||||
>
|
||||
返回首页
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 max-w-7xl mx-auto h-full w-full p-6">
|
||||
<div className="h-full flex gap-6">
|
||||
{/* Chat Area */}
|
||||
<div className="lg:col-span-3 flex flex-1 flex-col h-full">
|
||||
<div className="flex-1 flex flex-col h-full shadow-lg">
|
||||
<div className="pb-3 bg-white rounded-t-lg">
|
||||
<div className="flex items-center justify-between p-4">
|
||||
<span className="text-lg font-semibold">对话窗口</span>
|
||||
<div>
|
||||
<span className="w-2 h-2 bg-green-500 rounded-full mr-1 inline-block" />
|
||||
在线
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 flex flex-col justify-between h-full p-0 min-h-0">
|
||||
{/* Messages */}
|
||||
<div className="flex-1 overflow-y-auto p-6 bg-white">
|
||||
<div className="space-y-4 pb-4">
|
||||
{messages.map((message) => (
|
||||
<div
|
||||
key={message.id}
|
||||
className={`flex gap-3 ${
|
||||
message.type === "user"
|
||||
? "justify-end"
|
||||
: "justify-start"
|
||||
}`}
|
||||
>
|
||||
{message.type === "assistant" && (
|
||||
<div className="w-8 h-8 bg-gradient-to-br from-purple-500 to-pink-500 rounded-full flex items-center justify-center flex-shrink-0">
|
||||
<Bot className="w-4 h-4 text-white" />
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={`max-w-[80%] rounded-lg px-4 py-3 ${
|
||||
message.type === "user"
|
||||
? "bg-blue-500 text-white"
|
||||
: "bg-white text-gray-900 shadow-sm border border-gray-100"
|
||||
}`}
|
||||
>
|
||||
<div className="text-sm whitespace-pre-wrap">
|
||||
{formatMessage(message.content)}
|
||||
</div>
|
||||
{message.actions && message.actions.length > 0 && (
|
||||
<div className="mt-3 space-y-2">
|
||||
{message.actions.map((action, index) => (
|
||||
<Button
|
||||
key={index}
|
||||
type="default"
|
||||
size="small"
|
||||
className="mr-2 mb-2"
|
||||
onClick={() => handleActionClick(action)}
|
||||
>
|
||||
{action.label}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className="text-xs opacity-70 mt-2">
|
||||
{message.timestamp.toLocaleTimeString()}
|
||||
</div>
|
||||
</div>
|
||||
{message.type === "user" && (
|
||||
<div className="w-8 h-8 bg-blue-500 rounded-full flex items-center justify-center flex-shrink-0">
|
||||
<User className="w-4 h-4 text-white" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{isTyping && (
|
||||
<div className="flex gap-3 justify-start">
|
||||
<div className="w-8 h-8 bg-gradient-to-br from-purple-500 to-pink-500 rounded-full flex items-center justify-center">
|
||||
<Bot className="w-4 h-4 text-white" />
|
||||
</div>
|
||||
<div className="bg-white rounded-lg px-4 py-3 shadow-sm border border-gray-100">
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce"></div>
|
||||
<div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce delay-100"></div>
|
||||
<div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce delay-200"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
|
||||
{/* Input Area */}
|
||||
<div className="border-t border-gray-200 p-4 bg-white rounded-b-lg">
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
ref={inputRef}
|
||||
value={inputValue}
|
||||
onChange={(e) => setInputValue(e.target.value)}
|
||||
onKeyDown={handleKeyPress}
|
||||
placeholder="输入您的需求,例如:创建一个图像分类数据集..."
|
||||
disabled={isTyping}
|
||||
/>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => handleSendMessage(inputValue)}
|
||||
disabled={!inputValue.trim() || isTyping}
|
||||
className="bg-gradient-to-r from-purple-400 to-pink-400 border-none hover:from-purple-500 hover:to-pink-500"
|
||||
>
|
||||
<Send className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Quick Actions Sidebar */}
|
||||
<div className="w-72 flex flex-col gap-6">
|
||||
<Card className="shadow-lg">
|
||||
<div className="">
|
||||
<span className="text-lg font-semibold">快捷操作</span>
|
||||
<div className="text-sm text-gray-500">
|
||||
点击快速开始常用操作
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2 p-4">
|
||||
{quickActions.map((action) => (
|
||||
<Button
|
||||
key={action.id}
|
||||
type="default"
|
||||
className="w-full justify-start h-auto p-3 text-left"
|
||||
onClick={() => handleQuickAction(action)}
|
||||
>
|
||||
<action.icon className="w-4 h-4 mr-2 flex-shrink-0" />
|
||||
<div className="text-left">
|
||||
<div className="font-medium text-sm">
|
||||
{action.label}
|
||||
</div>
|
||||
</div>
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="shadow-lg">
|
||||
<div className="pb-3">
|
||||
<span className="text-lg font-semibold">系统状态</span>
|
||||
</div>
|
||||
<div className="space-y-3 p-4 pt-0">
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<CheckCircle className="w-4 h-4 text-green-500" />
|
||||
<span>AI服务正常</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<Clock className="w-4 h-4 text-blue-500" />
|
||||
<span>3个任务运行中</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<Database className="w-4 h-4 text-purple-500" />
|
||||
<span>12个数据集就绪</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<Zap className="w-4 h-4 text-orange-500" />
|
||||
<span>响应时间: 0.8s</span>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="shadow-lg">
|
||||
<div className="pb-3">
|
||||
<span className="text-lg font-semibold">使用提示</span>
|
||||
</div>
|
||||
<div className="space-y-2 text-sm text-gray-600 p-4 pt-0">
|
||||
<div>💡 您可以用自然语言描述需求</div>
|
||||
<div>🔍 支持复杂的多步骤操作</div>
|
||||
<div>📊 可以询问数据统计和分析</div>
|
||||
<div>⚡ 使用快捷操作提高效率</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="shadow-lg">
|
||||
<div className="pt-6 p-4">
|
||||
<Button
|
||||
type="default"
|
||||
className="w-full"
|
||||
icon={<HomeOutlined />}
|
||||
onClick={onBack}
|
||||
>
|
||||
返回主应用
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,229 +1,229 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Card, message } from "antd";
|
||||
import { Button, Badge, Progress, Checkbox } from "antd";
|
||||
import {
|
||||
ArrowLeft,
|
||||
FileText,
|
||||
ImageIcon,
|
||||
Video,
|
||||
Music,
|
||||
Save,
|
||||
SkipForward,
|
||||
CheckCircle,
|
||||
Eye,
|
||||
Settings,
|
||||
} from "lucide-react";
|
||||
import { mockTasks } from "@/mock/annotation";
|
||||
import { Outlet, useNavigate } from "react-router";
|
||||
|
||||
export default function AnnotationWorkspace() {
|
||||
const navigate = useNavigate();
|
||||
const [task, setTask] = useState(mockTasks[0]);
|
||||
|
||||
const [currentFileIndex, setCurrentFileIndex] = useState(0);
|
||||
const [annotationProgress, setAnnotationProgress] = useState({
|
||||
completed: task.completedCount,
|
||||
skipped: task.skippedCount,
|
||||
total: task.totalCount,
|
||||
});
|
||||
|
||||
const handleSaveAndNext = () => {
|
||||
setAnnotationProgress((prev) => ({
|
||||
...prev,
|
||||
completed: prev.completed + 1,
|
||||
}));
|
||||
|
||||
if (currentFileIndex < task.totalCount - 1) {
|
||||
setCurrentFileIndex(currentFileIndex + 1);
|
||||
}
|
||||
|
||||
message({
|
||||
title: "标注已保存",
|
||||
description: "标注结果已保存,自动跳转到下一个",
|
||||
});
|
||||
};
|
||||
|
||||
const handleSkipAndNext = () => {
|
||||
setAnnotationProgress((prev) => ({
|
||||
...prev,
|
||||
skipped: prev.skipped + 1,
|
||||
}));
|
||||
|
||||
if (currentFileIndex < task.totalCount - 1) {
|
||||
setCurrentFileIndex(currentFileIndex + 1);
|
||||
}
|
||||
|
||||
message({
|
||||
title: "已跳过",
|
||||
description: "已跳过当前项目,自动跳转到下一个",
|
||||
});
|
||||
};
|
||||
|
||||
const getDatasetTypeIcon = (type: string) => {
|
||||
switch (type) {
|
||||
case "text":
|
||||
return <FileText className="w-4 h-4 text-blue-500" />;
|
||||
case "image":
|
||||
return <ImageIcon className="w-4 h-4 text-green-500" />;
|
||||
case "video":
|
||||
return <Video className="w-4 h-4 text-purple-500" />;
|
||||
case "audio":
|
||||
return <Music className="w-4 h-4 text-orange-500" />;
|
||||
default:
|
||||
return <FileText className="w-4 h-4 text-gray-500" />;
|
||||
}
|
||||
};
|
||||
|
||||
const currentProgress = Math.round(
|
||||
((annotationProgress.completed + annotationProgress.skipped) /
|
||||
annotationProgress.total) *
|
||||
100
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="h-screen flex flex-col">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
type="text"
|
||||
onClick={() => navigate("/data/annotation")}
|
||||
icon={<ArrowLeft className="w-4 h-4" />}
|
||||
></Button>
|
||||
<div className="flex items-center space-x-2">
|
||||
{getDatasetTypeIcon(task.datasetType)}
|
||||
<span className="text-xl font-bold">{task.name}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="text-sm text-gray-600">
|
||||
{currentFileIndex + 1} / {task.totalCount}
|
||||
</div>
|
||||
<div className="flex items-center space-x-2 min-w-56">
|
||||
<span className="text-sm text-gray-600">进度:</span>
|
||||
<Progress
|
||||
percent={currentProgress}
|
||||
showInfo={false}
|
||||
className="w-24 h-2"
|
||||
/>
|
||||
<span className="text-sm font-medium">{currentProgress}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Content */}
|
||||
<div className="bg-white border-b border-gray-200 px-6 py-4 flex-1 flex">
|
||||
{/* Annotation Area */}
|
||||
<div className="flex-1 flex flex-col">
|
||||
<Outlet />
|
||||
</div>
|
||||
|
||||
{/* Right Sidebar - Only show for text and image types */}
|
||||
{(task.datasetType === "text" || task.datasetType === "image") && (
|
||||
<div className="w-80 border-l border-gray-200 p-4 space-y-4">
|
||||
{/* Progress Stats */}
|
||||
<Card>
|
||||
<div className="space-y-3 pt-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-gray-600">已完成</span>
|
||||
<span className="font-medium text-green-500">
|
||||
{annotationProgress.completed}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-gray-600">已跳过</span>
|
||||
<span className="font-medium text-red-500">
|
||||
{annotationProgress.skipped}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-gray-600">剩余</span>
|
||||
<span className="font-medium text-gray-600">
|
||||
{annotationProgress.total -
|
||||
annotationProgress.completed -
|
||||
annotationProgress.skipped}
|
||||
</span>
|
||||
</div>
|
||||
<div className="border-t border-gray-200 my-3" />
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-gray-600">总进度</span>
|
||||
<span className="font-medium">{currentProgress}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Quick Actions */}
|
||||
<Card>
|
||||
<div className="pt-4 space-y-2">
|
||||
<Button
|
||||
type="primary"
|
||||
block
|
||||
onClick={handleSaveAndNext}
|
||||
className="bg-green-500 border-green-500 hover:bg-green-600 hover:border-green-600"
|
||||
icon={<CheckCircle className="w-4 h-4 mr-2" />}
|
||||
>
|
||||
保存并下一个
|
||||
</Button>
|
||||
<Button
|
||||
block
|
||||
onClick={handleSkipAndNext}
|
||||
icon={<SkipForward className="w-4 h-4 mr-2" />}
|
||||
>
|
||||
跳过并下一个
|
||||
</Button>
|
||||
<Button block icon={<Save className="w-4 h-4 mr-2" />}>
|
||||
仅保存
|
||||
</Button>
|
||||
<Button block icon={<Eye className="w-4 h-4 mr-2" />}>
|
||||
预览结果
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Navigation */}
|
||||
<Card>
|
||||
<div className="pt-4 space-y-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button
|
||||
block
|
||||
disabled={currentFileIndex === 0}
|
||||
onClick={() => setCurrentFileIndex(currentFileIndex - 1)}
|
||||
>
|
||||
上一个
|
||||
</Button>
|
||||
<Button
|
||||
block
|
||||
disabled={currentFileIndex === task.totalCount - 1}
|
||||
onClick={() => setCurrentFileIndex(currentFileIndex + 1)}
|
||||
>
|
||||
下一个
|
||||
</Button>
|
||||
</div>
|
||||
<div className="text-xs text-gray-500">
|
||||
当前: {currentFileIndex + 1} / {task.totalCount}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Settings */}
|
||||
<Card>
|
||||
<div className="pt-4 space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm">自动保存</span>
|
||||
<Checkbox defaultChecked />
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm">快捷键提示</span>
|
||||
<Checkbox defaultChecked />
|
||||
</div>
|
||||
<Button block icon={<Settings className="w-4 h-4 mr-2" />}>
|
||||
更多设置
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
import { useEffect, useState } from "react";
|
||||
import { Card, message } from "antd";
|
||||
import { Button, Badge, Progress, Checkbox } from "antd";
|
||||
import {
|
||||
ArrowLeft,
|
||||
FileText,
|
||||
ImageIcon,
|
||||
Video,
|
||||
Music,
|
||||
Save,
|
||||
SkipForward,
|
||||
CheckCircle,
|
||||
Eye,
|
||||
Settings,
|
||||
} from "lucide-react";
|
||||
import { mockTasks } from "@/mock/annotation";
|
||||
import { Outlet, useNavigate } from "react-router";
|
||||
|
||||
export default function AnnotationWorkspace() {
|
||||
const navigate = useNavigate();
|
||||
const [task, setTask] = useState(mockTasks[0]);
|
||||
|
||||
const [currentFileIndex, setCurrentFileIndex] = useState(0);
|
||||
const [annotationProgress, setAnnotationProgress] = useState({
|
||||
completed: task.completedCount,
|
||||
skipped: task.skippedCount,
|
||||
total: task.totalCount,
|
||||
});
|
||||
|
||||
const handleSaveAndNext = () => {
|
||||
setAnnotationProgress((prev) => ({
|
||||
...prev,
|
||||
completed: prev.completed + 1,
|
||||
}));
|
||||
|
||||
if (currentFileIndex < task.totalCount - 1) {
|
||||
setCurrentFileIndex(currentFileIndex + 1);
|
||||
}
|
||||
|
||||
message({
|
||||
title: "标注已保存",
|
||||
description: "标注结果已保存,自动跳转到下一个",
|
||||
});
|
||||
};
|
||||
|
||||
const handleSkipAndNext = () => {
|
||||
setAnnotationProgress((prev) => ({
|
||||
...prev,
|
||||
skipped: prev.skipped + 1,
|
||||
}));
|
||||
|
||||
if (currentFileIndex < task.totalCount - 1) {
|
||||
setCurrentFileIndex(currentFileIndex + 1);
|
||||
}
|
||||
|
||||
message({
|
||||
title: "已跳过",
|
||||
description: "已跳过当前项目,自动跳转到下一个",
|
||||
});
|
||||
};
|
||||
|
||||
const getDatasetTypeIcon = (type: string) => {
|
||||
switch (type) {
|
||||
case "text":
|
||||
return <FileText className="w-4 h-4 text-blue-500" />;
|
||||
case "image":
|
||||
return <ImageIcon className="w-4 h-4 text-green-500" />;
|
||||
case "video":
|
||||
return <Video className="w-4 h-4 text-purple-500" />;
|
||||
case "audio":
|
||||
return <Music className="w-4 h-4 text-orange-500" />;
|
||||
default:
|
||||
return <FileText className="w-4 h-4 text-gray-500" />;
|
||||
}
|
||||
};
|
||||
|
||||
const currentProgress = Math.round(
|
||||
((annotationProgress.completed + annotationProgress.skipped) /
|
||||
annotationProgress.total) *
|
||||
100
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="h-screen flex flex-col">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
type="text"
|
||||
onClick={() => navigate("/data/annotation")}
|
||||
icon={<ArrowLeft className="w-4 h-4" />}
|
||||
></Button>
|
||||
<div className="flex items-center space-x-2">
|
||||
{getDatasetTypeIcon(task.datasetType)}
|
||||
<span className="text-xl font-bold">{task.name}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="text-sm text-gray-600">
|
||||
{currentFileIndex + 1} / {task.totalCount}
|
||||
</div>
|
||||
<div className="flex items-center space-x-2 min-w-56">
|
||||
<span className="text-sm text-gray-600">进度:</span>
|
||||
<Progress
|
||||
percent={currentProgress}
|
||||
showInfo={false}
|
||||
className="w-24 h-2"
|
||||
/>
|
||||
<span className="text-sm font-medium">{currentProgress}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Content */}
|
||||
<div className="bg-white border-b border-gray-200 px-6 py-4 flex-1 flex">
|
||||
{/* Annotation Area */}
|
||||
<div className="flex-1 flex flex-col">
|
||||
<Outlet />
|
||||
</div>
|
||||
|
||||
{/* Right Sidebar - Only show for text and image types */}
|
||||
{(task.datasetType === "text" || task.datasetType === "image") && (
|
||||
<div className="w-80 border-l border-gray-200 p-4 space-y-4">
|
||||
{/* Progress Stats */}
|
||||
<Card>
|
||||
<div className="space-y-3 pt-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-gray-600">已完成</span>
|
||||
<span className="font-medium text-green-500">
|
||||
{annotationProgress.completed}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-gray-600">已跳过</span>
|
||||
<span className="font-medium text-red-500">
|
||||
{annotationProgress.skipped}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-gray-600">剩余</span>
|
||||
<span className="font-medium text-gray-600">
|
||||
{annotationProgress.total -
|
||||
annotationProgress.completed -
|
||||
annotationProgress.skipped}
|
||||
</span>
|
||||
</div>
|
||||
<div className="border-t border-gray-200 my-3" />
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-gray-600">总进度</span>
|
||||
<span className="font-medium">{currentProgress}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Quick Actions */}
|
||||
<Card>
|
||||
<div className="pt-4 space-y-2">
|
||||
<Button
|
||||
type="primary"
|
||||
block
|
||||
onClick={handleSaveAndNext}
|
||||
className="bg-green-500 border-green-500 hover:bg-green-600 hover:border-green-600"
|
||||
icon={<CheckCircle className="w-4 h-4 mr-2" />}
|
||||
>
|
||||
保存并下一个
|
||||
</Button>
|
||||
<Button
|
||||
block
|
||||
onClick={handleSkipAndNext}
|
||||
icon={<SkipForward className="w-4 h-4 mr-2" />}
|
||||
>
|
||||
跳过并下一个
|
||||
</Button>
|
||||
<Button block icon={<Save className="w-4 h-4 mr-2" />}>
|
||||
仅保存
|
||||
</Button>
|
||||
<Button block icon={<Eye className="w-4 h-4 mr-2" />}>
|
||||
预览结果
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Navigation */}
|
||||
<Card>
|
||||
<div className="pt-4 space-y-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button
|
||||
block
|
||||
disabled={currentFileIndex === 0}
|
||||
onClick={() => setCurrentFileIndex(currentFileIndex - 1)}
|
||||
>
|
||||
上一个
|
||||
</Button>
|
||||
<Button
|
||||
block
|
||||
disabled={currentFileIndex === task.totalCount - 1}
|
||||
onClick={() => setCurrentFileIndex(currentFileIndex + 1)}
|
||||
>
|
||||
下一个
|
||||
</Button>
|
||||
</div>
|
||||
<div className="text-xs text-gray-500">
|
||||
当前: {currentFileIndex + 1} / {task.totalCount}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Settings */}
|
||||
<Card>
|
||||
<div className="pt-4 space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm">自动保存</span>
|
||||
<Checkbox defaultChecked />
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm">快捷键提示</span>
|
||||
<Checkbox defaultChecked />
|
||||
</div>
|
||||
<Button block icon={<Settings className="w-4 h-4 mr-2" />}>
|
||||
更多设置
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,457 +1,457 @@
|
||||
import { useState } from "react";
|
||||
import { Card, Button, Badge, Input, Checkbox } from "antd";
|
||||
|
||||
import {
|
||||
File,
|
||||
Search,
|
||||
CheckCircle,
|
||||
ThumbsUp,
|
||||
ThumbsDown,
|
||||
MessageSquare,
|
||||
HelpCircle,
|
||||
} from "lucide-react";
|
||||
|
||||
interface QAPair {
|
||||
id: string;
|
||||
question: string;
|
||||
answer: string;
|
||||
status: "pending" | "approved" | "rejected";
|
||||
confidence?: number;
|
||||
}
|
||||
|
||||
interface FileData {
|
||||
id: string;
|
||||
name: string;
|
||||
qaPairs: QAPair[];
|
||||
}
|
||||
|
||||
interface TextAnnotationWorkspaceProps {
|
||||
task: any;
|
||||
currentFileIndex: number;
|
||||
onSaveAndNext: () => void;
|
||||
onSkipAndNext: () => void;
|
||||
}
|
||||
|
||||
// 模拟文件数据
|
||||
const mockFiles: FileData[] = [
|
||||
{
|
||||
id: "1",
|
||||
name: "document_001.txt",
|
||||
qaPairs: [
|
||||
{
|
||||
id: "1",
|
||||
question: "什么是人工智能?",
|
||||
answer:
|
||||
"人工智能(AI)是计算机科学的一个分支,致力于创建能够执行通常需要人类智能的任务的系统。",
|
||||
status: "pending",
|
||||
confidence: 0.85,
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
question: "机器学习和深度学习有什么区别?",
|
||||
answer:
|
||||
"机器学习是人工智能的一个子集,而深度学习是机器学习的一个子集。深度学习使用神经网络来模拟人脑的工作方式。",
|
||||
status: "pending",
|
||||
confidence: 0.92,
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
question: "什么是神经网络?",
|
||||
answer:
|
||||
"神经网络是一种受生物神经网络启发的计算模型,由相互连接的节点(神经元)组成,能够学习和识别模式。",
|
||||
status: "pending",
|
||||
confidence: 0.78,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
name: "document_002.txt",
|
||||
qaPairs: [
|
||||
{
|
||||
id: "4",
|
||||
question: "什么是自然语言处理?",
|
||||
answer:
|
||||
"自然语言处理(NLP)是人工智能的一个分支,专注于使计算机能够理解、解释和生成人类语言。",
|
||||
status: "pending",
|
||||
confidence: 0.88,
|
||||
},
|
||||
{
|
||||
id: "5",
|
||||
question: "计算机视觉的应用有哪些?",
|
||||
answer:
|
||||
"计算机视觉广泛应用于图像识别、人脸识别、自动驾驶、医学影像分析、安防监控等领域。",
|
||||
status: "pending",
|
||||
confidence: 0.91,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export default function TextAnnotationWorkspace({
|
||||
onSaveAndNext,
|
||||
onSkipAndNext,
|
||||
}: TextAnnotationWorkspaceProps) {
|
||||
const [selectedFile, setSelectedFile] = useState<FileData | null>(
|
||||
mockFiles[0]
|
||||
);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [statusFilter, setStatusFilter] = useState("all");
|
||||
const [selectedQAs, setSelectedQAs] = useState<string[]>([]);
|
||||
|
||||
const handleFileSelect = (file: FileData) => {
|
||||
setSelectedFile(file);
|
||||
setSelectedQAs([]);
|
||||
};
|
||||
|
||||
const handleQAStatusChange = (
|
||||
qaId: string,
|
||||
status: "approved" | "rejected"
|
||||
) => {
|
||||
if (selectedFile) {
|
||||
const updatedFile = {
|
||||
...selectedFile,
|
||||
qaPairs: selectedFile.qaPairs.map((qa) =>
|
||||
qa.id === qaId ? { ...qa, status } : qa
|
||||
),
|
||||
};
|
||||
setSelectedFile(updatedFile);
|
||||
|
||||
message({
|
||||
title: status === "approved" ? "已标记为留用" : "已标记为不留用",
|
||||
description: `QA对 "${qaId}" 状态已更新`,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleBatchApprove = () => {
|
||||
if (selectedFile && selectedQAs.length > 0) {
|
||||
const updatedFile = {
|
||||
...selectedFile,
|
||||
qaPairs: selectedFile.qaPairs.map((qa) =>
|
||||
selectedQAs.includes(qa.id)
|
||||
? { ...qa, status: "approved" as const }
|
||||
: qa
|
||||
),
|
||||
};
|
||||
setSelectedFile(updatedFile);
|
||||
setSelectedQAs([]);
|
||||
|
||||
message({
|
||||
title: "批量操作完成",
|
||||
description: `已将 ${selectedQAs.length} 个QA对标记为留用`,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleBatchReject = () => {
|
||||
if (selectedFile && selectedQAs.length > 0) {
|
||||
const updatedFile = {
|
||||
...selectedFile,
|
||||
qaPairs: selectedFile.qaPairs.map((qa) =>
|
||||
selectedQAs.includes(qa.id)
|
||||
? { ...qa, status: "rejected" as const }
|
||||
: qa
|
||||
),
|
||||
};
|
||||
setSelectedFile(updatedFile);
|
||||
setSelectedQAs([]);
|
||||
|
||||
message({
|
||||
title: "批量操作完成",
|
||||
description: `已将 ${selectedQAs.length} 个QA对标记为不留用`,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleQASelect = (qaId: string, checked: boolean) => {
|
||||
if (checked) {
|
||||
setSelectedQAs([...selectedQAs, qaId]);
|
||||
} else {
|
||||
setSelectedQAs(selectedQAs.filter((id) => id !== qaId));
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectAll = (checked: boolean) => {
|
||||
if (checked && selectedFile) {
|
||||
setSelectedQAs(selectedFile.qaPairs.map((qa) => qa.id));
|
||||
} else {
|
||||
setSelectedQAs([]);
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusBadge = (status: string) => {
|
||||
switch (status) {
|
||||
case "approved":
|
||||
return <Badge className="bg-green-100 text-green-800">留用</Badge>;
|
||||
case "rejected":
|
||||
return <Badge className="bg-red-100 text-red-800">不留用</Badge>;
|
||||
default:
|
||||
return <Badge>待标注</Badge>;
|
||||
}
|
||||
};
|
||||
|
||||
const getConfidenceColor = (confidence?: number) => {
|
||||
if (!confidence) return "text-gray-500";
|
||||
if (confidence >= 0.8) return "text-green-600";
|
||||
if (confidence >= 0.6) return "text-yellow-600";
|
||||
return "text-red-600";
|
||||
};
|
||||
|
||||
const filteredQAs =
|
||||
selectedFile?.qaPairs.filter((qa) => {
|
||||
const matchesSearch =
|
||||
qa.question.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
qa.answer.toLowerCase().includes(searchQuery.toLowerCase());
|
||||
const matchesStatus =
|
||||
statusFilter === "all" || qa.status === statusFilter;
|
||||
return matchesSearch && matchesStatus;
|
||||
}) || [];
|
||||
|
||||
return (
|
||||
<div className="flex-1 flex">
|
||||
{/* File List */}
|
||||
<div className="w-80 border-r bg-gray-50 p-4">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h3 className="font-medium mb-2">文件列表</h3>
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
|
||||
<Input placeholder="搜索文件..." className="pl-10" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="h-96">
|
||||
<div className="space-y-2">
|
||||
{mockFiles.map((file) => (
|
||||
<div
|
||||
key={file.id}
|
||||
className={`p-3 border rounded-lg cursor-pointer transition-colors ${
|
||||
selectedFile?.id === file.id
|
||||
? "border-blue-500 bg-blue-50"
|
||||
: "border-gray-200 hover:border-gray-300"
|
||||
}`}
|
||||
onClick={() => handleFileSelect(file)}
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<File className="w-4 h-4 text-gray-400" />
|
||||
<div className="flex-1">
|
||||
<div className="font-medium text-sm">{file.name}</div>
|
||||
<div className="text-xs text-gray-500">
|
||||
{file.qaPairs.length} 个QA对
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* QA Annotation Area */}
|
||||
<div className="flex-1 p-6">
|
||||
{selectedFile ? (
|
||||
<div className="space-y-4">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-xl font-bold">{selectedFile.name}</h2>
|
||||
<p className="text-gray-500">
|
||||
共 {selectedFile.qaPairs.length} 个QA对
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button
|
||||
onClick={onSaveAndNext}
|
||||
className="bg-green-600 hover:bg-green-700"
|
||||
>
|
||||
<CheckCircle className="w-4 h-4 mr-2" />
|
||||
保存并下一个
|
||||
</Button>
|
||||
<Button onClick={onSkipAndNext}>跳过</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Filters and Batch Actions */}
|
||||
<Card>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
|
||||
<Input
|
||||
placeholder="搜索QA对..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-10 w-64"
|
||||
/>
|
||||
</div>
|
||||
<select
|
||||
value={statusFilter}
|
||||
onChange={(e) => setStatusFilter(e.target.value)}
|
||||
className="px-3 py-2 border rounded-md text-sm"
|
||||
>
|
||||
<option value="all">全部状态</option>
|
||||
<option value="pending">待标注</option>
|
||||
<option value="approved">已留用</option>
|
||||
<option value="rejected">不留用</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{selectedQAs.length > 0 && (
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="text-sm text-gray-600">
|
||||
已选择 {selectedQAs.length} 个
|
||||
</span>
|
||||
<Button
|
||||
onClick={handleBatchApprove}
|
||||
size="sm"
|
||||
className="bg-green-600 hover:bg-green-700"
|
||||
>
|
||||
<ThumbsUp className="w-4 h-4 mr-1" />
|
||||
批量留用
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleBatchReject}
|
||||
size="sm"
|
||||
variant="destructive"
|
||||
>
|
||||
<ThumbsDown className="w-4 h-4 mr-1" />
|
||||
批量不留用
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* QA List */}
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
checked={
|
||||
selectedQAs.length === filteredQAs.length &&
|
||||
filteredQAs.length > 0
|
||||
}
|
||||
onChange={handleSelectAll}
|
||||
/>
|
||||
<span className="text-sm font-medium">全选</span>
|
||||
</div>
|
||||
|
||||
<div className="h-500">
|
||||
<div className="space-y-4">
|
||||
{filteredQAs.map((qa) => (
|
||||
<Card
|
||||
key={qa.id}
|
||||
className="hover:shadow-md transition-shadow"
|
||||
>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
checked={selectedQAs.includes(qa.id)}
|
||||
onCheckedChange={(checked) =>
|
||||
handleQASelect(qa.id, checked as boolean)
|
||||
}
|
||||
/>
|
||||
<MessageSquare className="w-4 h-4 text-blue-500" />
|
||||
<span className="text-sm font-medium">
|
||||
QA-{qa.id}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
{qa.confidence && (
|
||||
<span
|
||||
className={`text-xs ${getConfidenceColor(
|
||||
qa.confidence
|
||||
)}`}
|
||||
>
|
||||
置信度: {(qa.confidence * 100).toFixed(1)}%
|
||||
</span>
|
||||
)}
|
||||
{getStatusBadge(qa.status)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div>
|
||||
<div className="flex items-center space-x-2 mb-1">
|
||||
<HelpCircle className="w-4 h-4 text-blue-500" />
|
||||
<span className="text-sm font-medium text-blue-700">
|
||||
问题
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm bg-blue-50 p-3 rounded">
|
||||
{qa.question}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="flex items-center space-x-2 mb-1">
|
||||
<MessageSquare className="w-4 h-4 text-green-500" />
|
||||
<span className="text-sm font-medium text-green-700">
|
||||
答案
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm bg-green-50 p-3 rounded">
|
||||
{qa.answer}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-end space-x-2">
|
||||
<Button
|
||||
onClick={() =>
|
||||
handleQAStatusChange(qa.id, "approved")
|
||||
}
|
||||
size="sm"
|
||||
variant={
|
||||
qa.status === "approved" ? "default" : "outline"
|
||||
}
|
||||
className={
|
||||
qa.status === "approved"
|
||||
? "bg-green-600 hover:bg-green-700"
|
||||
: ""
|
||||
}
|
||||
>
|
||||
<ThumbsUp className="w-4 h-4 mr-1" />
|
||||
留用
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() =>
|
||||
handleQAStatusChange(qa.id, "rejected")
|
||||
}
|
||||
size="sm"
|
||||
variant={
|
||||
qa.status === "rejected"
|
||||
? "destructive"
|
||||
: "outline"
|
||||
}
|
||||
>
|
||||
<ThumbsDown className="w-4 h-4 mr-1" />
|
||||
不留用
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex-1 flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<File className="w-12 h-12 text-gray-400 mx-auto mb-4" />
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">
|
||||
选择文件开始标注
|
||||
</h3>
|
||||
<p className="text-gray-500">
|
||||
从左侧文件列表中选择一个文件开始标注工作
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
import { useState } from "react";
|
||||
import { Card, Button, Badge, Input, Checkbox } from "antd";
|
||||
|
||||
import {
|
||||
File,
|
||||
Search,
|
||||
CheckCircle,
|
||||
ThumbsUp,
|
||||
ThumbsDown,
|
||||
MessageSquare,
|
||||
HelpCircle,
|
||||
} from "lucide-react";
|
||||
|
||||
interface QAPair {
|
||||
id: string;
|
||||
question: string;
|
||||
answer: string;
|
||||
status: "pending" | "approved" | "rejected";
|
||||
confidence?: number;
|
||||
}
|
||||
|
||||
interface FileData {
|
||||
id: string;
|
||||
name: string;
|
||||
qaPairs: QAPair[];
|
||||
}
|
||||
|
||||
interface TextAnnotationWorkspaceProps {
|
||||
task: any;
|
||||
currentFileIndex: number;
|
||||
onSaveAndNext: () => void;
|
||||
onSkipAndNext: () => void;
|
||||
}
|
||||
|
||||
// 模拟文件数据
|
||||
const mockFiles: FileData[] = [
|
||||
{
|
||||
id: "1",
|
||||
name: "document_001.txt",
|
||||
qaPairs: [
|
||||
{
|
||||
id: "1",
|
||||
question: "什么是人工智能?",
|
||||
answer:
|
||||
"人工智能(AI)是计算机科学的一个分支,致力于创建能够执行通常需要人类智能的任务的系统。",
|
||||
status: "pending",
|
||||
confidence: 0.85,
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
question: "机器学习和深度学习有什么区别?",
|
||||
answer:
|
||||
"机器学习是人工智能的一个子集,而深度学习是机器学习的一个子集。深度学习使用神经网络来模拟人脑的工作方式。",
|
||||
status: "pending",
|
||||
confidence: 0.92,
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
question: "什么是神经网络?",
|
||||
answer:
|
||||
"神经网络是一种受生物神经网络启发的计算模型,由相互连接的节点(神经元)组成,能够学习和识别模式。",
|
||||
status: "pending",
|
||||
confidence: 0.78,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
name: "document_002.txt",
|
||||
qaPairs: [
|
||||
{
|
||||
id: "4",
|
||||
question: "什么是自然语言处理?",
|
||||
answer:
|
||||
"自然语言处理(NLP)是人工智能的一个分支,专注于使计算机能够理解、解释和生成人类语言。",
|
||||
status: "pending",
|
||||
confidence: 0.88,
|
||||
},
|
||||
{
|
||||
id: "5",
|
||||
question: "计算机视觉的应用有哪些?",
|
||||
answer:
|
||||
"计算机视觉广泛应用于图像识别、人脸识别、自动驾驶、医学影像分析、安防监控等领域。",
|
||||
status: "pending",
|
||||
confidence: 0.91,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export default function TextAnnotationWorkspace({
|
||||
onSaveAndNext,
|
||||
onSkipAndNext,
|
||||
}: TextAnnotationWorkspaceProps) {
|
||||
const [selectedFile, setSelectedFile] = useState<FileData | null>(
|
||||
mockFiles[0]
|
||||
);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [statusFilter, setStatusFilter] = useState("all");
|
||||
const [selectedQAs, setSelectedQAs] = useState<string[]>([]);
|
||||
|
||||
const handleFileSelect = (file: FileData) => {
|
||||
setSelectedFile(file);
|
||||
setSelectedQAs([]);
|
||||
};
|
||||
|
||||
const handleQAStatusChange = (
|
||||
qaId: string,
|
||||
status: "approved" | "rejected"
|
||||
) => {
|
||||
if (selectedFile) {
|
||||
const updatedFile = {
|
||||
...selectedFile,
|
||||
qaPairs: selectedFile.qaPairs.map((qa) =>
|
||||
qa.id === qaId ? { ...qa, status } : qa
|
||||
),
|
||||
};
|
||||
setSelectedFile(updatedFile);
|
||||
|
||||
message({
|
||||
title: status === "approved" ? "已标记为留用" : "已标记为不留用",
|
||||
description: `QA对 "${qaId}" 状态已更新`,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleBatchApprove = () => {
|
||||
if (selectedFile && selectedQAs.length > 0) {
|
||||
const updatedFile = {
|
||||
...selectedFile,
|
||||
qaPairs: selectedFile.qaPairs.map((qa) =>
|
||||
selectedQAs.includes(qa.id)
|
||||
? { ...qa, status: "approved" as const }
|
||||
: qa
|
||||
),
|
||||
};
|
||||
setSelectedFile(updatedFile);
|
||||
setSelectedQAs([]);
|
||||
|
||||
message({
|
||||
title: "批量操作完成",
|
||||
description: `已将 ${selectedQAs.length} 个QA对标记为留用`,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleBatchReject = () => {
|
||||
if (selectedFile && selectedQAs.length > 0) {
|
||||
const updatedFile = {
|
||||
...selectedFile,
|
||||
qaPairs: selectedFile.qaPairs.map((qa) =>
|
||||
selectedQAs.includes(qa.id)
|
||||
? { ...qa, status: "rejected" as const }
|
||||
: qa
|
||||
),
|
||||
};
|
||||
setSelectedFile(updatedFile);
|
||||
setSelectedQAs([]);
|
||||
|
||||
message({
|
||||
title: "批量操作完成",
|
||||
description: `已将 ${selectedQAs.length} 个QA对标记为不留用`,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleQASelect = (qaId: string, checked: boolean) => {
|
||||
if (checked) {
|
||||
setSelectedQAs([...selectedQAs, qaId]);
|
||||
} else {
|
||||
setSelectedQAs(selectedQAs.filter((id) => id !== qaId));
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectAll = (checked: boolean) => {
|
||||
if (checked && selectedFile) {
|
||||
setSelectedQAs(selectedFile.qaPairs.map((qa) => qa.id));
|
||||
} else {
|
||||
setSelectedQAs([]);
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusBadge = (status: string) => {
|
||||
switch (status) {
|
||||
case "approved":
|
||||
return <Badge className="bg-green-100 text-green-800">留用</Badge>;
|
||||
case "rejected":
|
||||
return <Badge className="bg-red-100 text-red-800">不留用</Badge>;
|
||||
default:
|
||||
return <Badge>待标注</Badge>;
|
||||
}
|
||||
};
|
||||
|
||||
const getConfidenceColor = (confidence?: number) => {
|
||||
if (!confidence) return "text-gray-500";
|
||||
if (confidence >= 0.8) return "text-green-600";
|
||||
if (confidence >= 0.6) return "text-yellow-600";
|
||||
return "text-red-600";
|
||||
};
|
||||
|
||||
const filteredQAs =
|
||||
selectedFile?.qaPairs.filter((qa) => {
|
||||
const matchesSearch =
|
||||
qa.question.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
qa.answer.toLowerCase().includes(searchQuery.toLowerCase());
|
||||
const matchesStatus =
|
||||
statusFilter === "all" || qa.status === statusFilter;
|
||||
return matchesSearch && matchesStatus;
|
||||
}) || [];
|
||||
|
||||
return (
|
||||
<div className="flex-1 flex">
|
||||
{/* File List */}
|
||||
<div className="w-80 border-r bg-gray-50 p-4">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h3 className="font-medium mb-2">文件列表</h3>
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
|
||||
<Input placeholder="搜索文件..." className="pl-10" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="h-96">
|
||||
<div className="space-y-2">
|
||||
{mockFiles.map((file) => (
|
||||
<div
|
||||
key={file.id}
|
||||
className={`p-3 border rounded-lg cursor-pointer transition-colors ${
|
||||
selectedFile?.id === file.id
|
||||
? "border-blue-500 bg-blue-50"
|
||||
: "border-gray-200 hover:border-gray-300"
|
||||
}`}
|
||||
onClick={() => handleFileSelect(file)}
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<File className="w-4 h-4 text-gray-400" />
|
||||
<div className="flex-1">
|
||||
<div className="font-medium text-sm">{file.name}</div>
|
||||
<div className="text-xs text-gray-500">
|
||||
{file.qaPairs.length} 个QA对
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* QA Annotation Area */}
|
||||
<div className="flex-1 p-6">
|
||||
{selectedFile ? (
|
||||
<div className="space-y-4">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-xl font-bold">{selectedFile.name}</h2>
|
||||
<p className="text-gray-500">
|
||||
共 {selectedFile.qaPairs.length} 个QA对
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button
|
||||
onClick={onSaveAndNext}
|
||||
className="bg-green-600 hover:bg-green-700"
|
||||
>
|
||||
<CheckCircle className="w-4 h-4 mr-2" />
|
||||
保存并下一个
|
||||
</Button>
|
||||
<Button onClick={onSkipAndNext}>跳过</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Filters and Batch Actions */}
|
||||
<Card>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
|
||||
<Input
|
||||
placeholder="搜索QA对..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-10 w-64"
|
||||
/>
|
||||
</div>
|
||||
<select
|
||||
value={statusFilter}
|
||||
onChange={(e) => setStatusFilter(e.target.value)}
|
||||
className="px-3 py-2 border rounded-md text-sm"
|
||||
>
|
||||
<option value="all">全部状态</option>
|
||||
<option value="pending">待标注</option>
|
||||
<option value="approved">已留用</option>
|
||||
<option value="rejected">不留用</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{selectedQAs.length > 0 && (
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="text-sm text-gray-600">
|
||||
已选择 {selectedQAs.length} 个
|
||||
</span>
|
||||
<Button
|
||||
onClick={handleBatchApprove}
|
||||
size="sm"
|
||||
className="bg-green-600 hover:bg-green-700"
|
||||
>
|
||||
<ThumbsUp className="w-4 h-4 mr-1" />
|
||||
批量留用
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleBatchReject}
|
||||
size="sm"
|
||||
variant="destructive"
|
||||
>
|
||||
<ThumbsDown className="w-4 h-4 mr-1" />
|
||||
批量不留用
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* QA List */}
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
checked={
|
||||
selectedQAs.length === filteredQAs.length &&
|
||||
filteredQAs.length > 0
|
||||
}
|
||||
onChange={handleSelectAll}
|
||||
/>
|
||||
<span className="text-sm font-medium">全选</span>
|
||||
</div>
|
||||
|
||||
<div className="h-500">
|
||||
<div className="space-y-4">
|
||||
{filteredQAs.map((qa) => (
|
||||
<Card
|
||||
key={qa.id}
|
||||
className="hover:shadow-md transition-shadow"
|
||||
>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
checked={selectedQAs.includes(qa.id)}
|
||||
onCheckedChange={(checked) =>
|
||||
handleQASelect(qa.id, checked as boolean)
|
||||
}
|
||||
/>
|
||||
<MessageSquare className="w-4 h-4 text-blue-500" />
|
||||
<span className="text-sm font-medium">
|
||||
QA-{qa.id}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
{qa.confidence && (
|
||||
<span
|
||||
className={`text-xs ${getConfidenceColor(
|
||||
qa.confidence
|
||||
)}`}
|
||||
>
|
||||
置信度: {(qa.confidence * 100).toFixed(1)}%
|
||||
</span>
|
||||
)}
|
||||
{getStatusBadge(qa.status)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div>
|
||||
<div className="flex items-center space-x-2 mb-1">
|
||||
<HelpCircle className="w-4 h-4 text-blue-500" />
|
||||
<span className="text-sm font-medium text-blue-700">
|
||||
问题
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm bg-blue-50 p-3 rounded">
|
||||
{qa.question}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="flex items-center space-x-2 mb-1">
|
||||
<MessageSquare className="w-4 h-4 text-green-500" />
|
||||
<span className="text-sm font-medium text-green-700">
|
||||
答案
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm bg-green-50 p-3 rounded">
|
||||
{qa.answer}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-end space-x-2">
|
||||
<Button
|
||||
onClick={() =>
|
||||
handleQAStatusChange(qa.id, "approved")
|
||||
}
|
||||
size="sm"
|
||||
variant={
|
||||
qa.status === "approved" ? "default" : "outline"
|
||||
}
|
||||
className={
|
||||
qa.status === "approved"
|
||||
? "bg-green-600 hover:bg-green-700"
|
||||
: ""
|
||||
}
|
||||
>
|
||||
<ThumbsUp className="w-4 h-4 mr-1" />
|
||||
留用
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() =>
|
||||
handleQAStatusChange(qa.id, "rejected")
|
||||
}
|
||||
size="sm"
|
||||
variant={
|
||||
qa.status === "rejected"
|
||||
? "destructive"
|
||||
: "outline"
|
||||
}
|
||||
>
|
||||
<ThumbsDown className="w-4 h-4 mr-1" />
|
||||
不留用
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex-1 flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<File className="w-12 h-12 text-gray-400 mx-auto mb-4" />
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">
|
||||
选择文件开始标注
|
||||
</h3>
|
||||
<p className="text-gray-500">
|
||||
从左侧文件列表中选择一个文件开始标注工作
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,346 +1,346 @@
|
||||
import type React from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Card, Button, Input, Select, Divider, Form, message } from "antd";
|
||||
import TextArea from "antd/es/input/TextArea";
|
||||
import {
|
||||
DatabaseOutlined,
|
||||
CheckOutlined,
|
||||
PlusOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { mockTemplates } from "@/mock/annotation";
|
||||
import CustomTemplateDialog from "./components/CustomTemplateDialog";
|
||||
import { Link, useNavigate } from "react-router";
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
import { queryDatasetsUsingGet } from "../../DataManagement/dataset.api";
|
||||
import {
|
||||
DatasetType,
|
||||
type Dataset,
|
||||
} from "@/pages/DataManagement/dataset.model";
|
||||
|
||||
interface Template {
|
||||
id: string;
|
||||
name: string;
|
||||
category: string;
|
||||
description: string;
|
||||
type: "text" | "image";
|
||||
preview?: string;
|
||||
icon: React.ReactNode;
|
||||
isCustom?: boolean;
|
||||
}
|
||||
|
||||
const templateCategories = ["Computer Vision", "Natural Language Processing"];
|
||||
|
||||
export default function AnnotationTaskCreate() {
|
||||
const navigate = useNavigate();
|
||||
const [form] = Form.useForm();
|
||||
const [showCustomTemplateDialog, setShowCustomTemplateDialog] =
|
||||
useState(false);
|
||||
const [selectedCategory, setSelectedCategory] = useState("Computer Vision");
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [datasetFilter, setDatasetFilter] = useState("all");
|
||||
const [selectedTemplate, setSelectedTemplate] = useState<Template | null>(
|
||||
null
|
||||
);
|
||||
const [datasets, setDatasets] = useState<Dataset[]>([]);
|
||||
const [selectedDataset, setSelectedDataset] = useState<Dataset | null>(null);
|
||||
|
||||
// 用于Form的受控数据
|
||||
const [formValues, setFormValues] = useState({
|
||||
name: "",
|
||||
description: "",
|
||||
datasetId: "",
|
||||
templateId: "",
|
||||
});
|
||||
|
||||
const fetchDatasets = async () => {
|
||||
const { data } = await queryDatasetsUsingGet();
|
||||
setDatasets(data.results || []);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchDatasets();
|
||||
}, []);
|
||||
|
||||
const filteredTemplates = mockTemplates.filter(
|
||||
(template) => template.category === selectedCategory
|
||||
);
|
||||
|
||||
const handleDatasetSelect = (datasetId: string) => {
|
||||
const dataset = datasets.find((ds) => ds.id === datasetId) || null;
|
||||
setSelectedDataset(dataset);
|
||||
setFormValues((prev) => ({ ...prev, datasetId }));
|
||||
if (dataset?.type === DatasetType.PRETRAIN_IMAGE) {
|
||||
setSelectedCategory("Computer Vision");
|
||||
} else if (dataset?.type === DatasetType.PRETRAIN_TEXT) {
|
||||
setSelectedCategory("Natural Language Processing");
|
||||
}
|
||||
setSelectedTemplate(null);
|
||||
setFormValues((prev) => ({ ...prev, templateId: "" }));
|
||||
};
|
||||
|
||||
const handleTemplateSelect = (template: Template) => {
|
||||
setSelectedTemplate(template);
|
||||
setFormValues((prev) => ({ ...prev, templateId: template.id }));
|
||||
};
|
||||
|
||||
const handleValuesChange = (_, allValues) => {
|
||||
setFormValues({ ...formValues, ...allValues });
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
const dataset = datasets.find((ds) => ds.id === values.datasetId);
|
||||
const template = mockTemplates.find(
|
||||
(tpl) => tpl.id === values.templateId
|
||||
);
|
||||
if (!dataset) {
|
||||
message.error("请选择数据集");
|
||||
return;
|
||||
}
|
||||
if (!template) {
|
||||
message.error("请选择标注模板");
|
||||
return;
|
||||
}
|
||||
const taskData = {
|
||||
name: values.name,
|
||||
description: values.description,
|
||||
dataset,
|
||||
template,
|
||||
};
|
||||
// onCreateTask(taskData); // 实际创建逻辑
|
||||
message.success("标注任务创建成功");
|
||||
navigate("/data/annotation");
|
||||
} catch (e) {
|
||||
// 校验失败
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveCustomTemplate = (templateData: any) => {
|
||||
setSelectedTemplate(templateData);
|
||||
setFormValues((prev) => ({ ...prev, templateId: templateData.id }));
|
||||
message.success(`自定义模板 "${templateData.name}" 已创建`);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex-overflow-auto">
|
||||
{/* Header */}
|
||||
<div className="flex items-center mb-2">
|
||||
<Link to="/data/annotation">
|
||||
<Button type="text">
|
||||
<ArrowLeft className="w-4 h-4 mr-1" />
|
||||
</Button>
|
||||
</Link>
|
||||
<h1 className="text-xl font-bold bg-clip-text">创建标注任务</h1>
|
||||
</div>
|
||||
|
||||
<div className="flex-overflow-auto bg-white rounded-lg shadow-sm">
|
||||
<div className="flex-1 overflow-y-auto p-6">
|
||||
<Form
|
||||
form={form}
|
||||
initialValues={formValues}
|
||||
onValuesChange={handleValuesChange}
|
||||
layout="vertical"
|
||||
>
|
||||
{/* 基本信息 */}
|
||||
<h2 className="font-medium text-gray-900 text-lg mb-2">基本信息</h2>
|
||||
<Form.Item
|
||||
label="任务名称"
|
||||
name="name"
|
||||
rules={[{ required: true, message: "请输入任务名称" }]}
|
||||
>
|
||||
<Input placeholder="输入任务名称" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="任务描述"
|
||||
name="description"
|
||||
rules={[{ required: true, message: "请输入任务描述" }]}
|
||||
>
|
||||
<TextArea placeholder="详细描述标注任务的要求和目标" rows={3} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="选择数据集"
|
||||
name="datasetId"
|
||||
rules={[{ required: true, message: "请选择数据集" }]}
|
||||
>
|
||||
<Select
|
||||
optionFilterProp="children"
|
||||
value={formValues.datasetId}
|
||||
onChange={handleDatasetSelect}
|
||||
placeholder="请选择数据集"
|
||||
size="large"
|
||||
options={datasets.map((dataset) => ({
|
||||
label: (
|
||||
<div className="flex items-center justify-between gap-3 py-2">
|
||||
<div className="font-medium text-gray-900">
|
||||
{dataset?.icon || <DatabaseOutlined className="mr-2" />}
|
||||
{dataset.name}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500">
|
||||
{dataset?.fileCount} 文件 • {dataset.size}
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
value: dataset.id,
|
||||
}))}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
{/* 模板选择 */}
|
||||
<h2 className="font-medium text-gray-900 text-lg mt-6 mb-2 flex items-center gap-2">
|
||||
模板选择
|
||||
</h2>
|
||||
<Form.Item
|
||||
name="templateId"
|
||||
rules={[{ required: true, message: "请选择标注模板" }]}
|
||||
>
|
||||
<div className="flex">
|
||||
{/* Category Sidebar */}
|
||||
<div className="w-64 pr-6 border-r border-gray-200">
|
||||
<div className="space-y-2">
|
||||
{templateCategories.map((category) => {
|
||||
const isAvailable =
|
||||
selectedDataset?.type === "image"
|
||||
? category === "Computer Vision"
|
||||
: category === "Natural Language Processing";
|
||||
return (
|
||||
<Button
|
||||
key={category}
|
||||
type={
|
||||
selectedCategory === category && isAvailable
|
||||
? "primary"
|
||||
: "default"
|
||||
}
|
||||
block
|
||||
disabled={!isAvailable}
|
||||
onClick={() =>
|
||||
isAvailable && setSelectedCategory(category)
|
||||
}
|
||||
style={{ textAlign: "left", marginBottom: 8 }}
|
||||
>
|
||||
{category}
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
<Button
|
||||
type="dashed"
|
||||
block
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => setShowCustomTemplateDialog(true)}
|
||||
>
|
||||
自定义模板
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{/* Template Grid */}
|
||||
<div className="flex-1 pl-6">
|
||||
<div className="max-h-96 overflow-auto">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{filteredTemplates.map((template) => (
|
||||
<div
|
||||
key={template.id}
|
||||
className={`border rounded-lg cursor-pointer transition-all hover:shadow-md ${
|
||||
formValues.templateId === template.id
|
||||
? "border-blue-500 bg-blue-50"
|
||||
: "border-gray-200"
|
||||
}`}
|
||||
onClick={() => handleTemplateSelect(template)}
|
||||
>
|
||||
{template.preview && (
|
||||
<div className="aspect-video bg-gray-100 rounded-t-lg overflow-hidden">
|
||||
<img
|
||||
src={template.preview || "/placeholder.svg"}
|
||||
alt={template.name}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="p-3">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
{template.icon}
|
||||
<span className="font-medium text-sm">
|
||||
{template.name}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-gray-600">
|
||||
{template.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{/* Custom Template Option */}
|
||||
<div
|
||||
className={`border-2 border-dashed rounded-lg cursor-pointer transition-all hover:border-gray-400 ${
|
||||
selectedTemplate?.isCustom
|
||||
? "border-blue-500 bg-blue-50"
|
||||
: "border-gray-300"
|
||||
}`}
|
||||
onClick={() => setShowCustomTemplateDialog(true)}
|
||||
>
|
||||
<div className="aspect-video bg-gray-50 rounded-t-lg flex items-center justify-center">
|
||||
<PlusOutlined
|
||||
style={{ fontSize: 32, color: "#bbb" }}
|
||||
/>
|
||||
</div>
|
||||
<div className="p-3">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
<PlusOutlined />
|
||||
<span className="font-medium text-sm">
|
||||
自定义模板
|
||||
</span>
|
||||
</div>
|
||||
{selectedTemplate?.isCustom && (
|
||||
<CheckOutlined style={{ color: "#1677ff" }} />
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-gray-600">
|
||||
创建符合特定需求的标注模板
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{selectedTemplate && (
|
||||
<div className="mt-4 p-3 bg-blue-50 rounded-lg">
|
||||
<div className="flex items-center space-x-2">
|
||||
<span
|
||||
className="text-sm font-medium"
|
||||
style={{ color: "#1677ff" }}
|
||||
>
|
||||
已选择模板
|
||||
</span>
|
||||
</div>
|
||||
<p
|
||||
className="text-sm"
|
||||
style={{ color: "#1677ff", marginTop: 4 }}
|
||||
>
|
||||
{selectedTemplate.name} - {selectedTemplate.description}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</div>
|
||||
<div className="flex gap-2 justify-end border-t border-gray-200 p-6">
|
||||
<Button onClick={() => navigate("/data/annotation")}>取消</Button>
|
||||
<Button type="primary" onClick={handleSubmit}>
|
||||
创建任务
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Custom Template Dialog */}
|
||||
<CustomTemplateDialog
|
||||
open={showCustomTemplateDialog}
|
||||
onOpenChange={setShowCustomTemplateDialog}
|
||||
onSaveTemplate={handleSaveCustomTemplate}
|
||||
datasetType={selectedDataset?.type || "image"}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
import type React from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Card, Button, Input, Select, Divider, Form, message } from "antd";
|
||||
import TextArea from "antd/es/input/TextArea";
|
||||
import {
|
||||
DatabaseOutlined,
|
||||
CheckOutlined,
|
||||
PlusOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { mockTemplates } from "@/mock/annotation";
|
||||
import CustomTemplateDialog from "./components/CustomTemplateDialog";
|
||||
import { Link, useNavigate } from "react-router";
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
import { queryDatasetsUsingGet } from "../../DataManagement/dataset.api";
|
||||
import {
|
||||
DatasetType,
|
||||
type Dataset,
|
||||
} from "@/pages/DataManagement/dataset.model";
|
||||
|
||||
interface Template {
|
||||
id: string;
|
||||
name: string;
|
||||
category: string;
|
||||
description: string;
|
||||
type: "text" | "image";
|
||||
preview?: string;
|
||||
icon: React.ReactNode;
|
||||
isCustom?: boolean;
|
||||
}
|
||||
|
||||
const templateCategories = ["Computer Vision", "Natural Language Processing"];
|
||||
|
||||
export default function AnnotationTaskCreate() {
|
||||
const navigate = useNavigate();
|
||||
const [form] = Form.useForm();
|
||||
const [showCustomTemplateDialog, setShowCustomTemplateDialog] =
|
||||
useState(false);
|
||||
const [selectedCategory, setSelectedCategory] = useState("Computer Vision");
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [datasetFilter, setDatasetFilter] = useState("all");
|
||||
const [selectedTemplate, setSelectedTemplate] = useState<Template | null>(
|
||||
null
|
||||
);
|
||||
const [datasets, setDatasets] = useState<Dataset[]>([]);
|
||||
const [selectedDataset, setSelectedDataset] = useState<Dataset | null>(null);
|
||||
|
||||
// 用于Form的受控数据
|
||||
const [formValues, setFormValues] = useState({
|
||||
name: "",
|
||||
description: "",
|
||||
datasetId: "",
|
||||
templateId: "",
|
||||
});
|
||||
|
||||
const fetchDatasets = async () => {
|
||||
const { data } = await queryDatasetsUsingGet();
|
||||
setDatasets(data.results || []);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchDatasets();
|
||||
}, []);
|
||||
|
||||
const filteredTemplates = mockTemplates.filter(
|
||||
(template) => template.category === selectedCategory
|
||||
);
|
||||
|
||||
const handleDatasetSelect = (datasetId: string) => {
|
||||
const dataset = datasets.find((ds) => ds.id === datasetId) || null;
|
||||
setSelectedDataset(dataset);
|
||||
setFormValues((prev) => ({ ...prev, datasetId }));
|
||||
if (dataset?.type === DatasetType.PRETRAIN_IMAGE) {
|
||||
setSelectedCategory("Computer Vision");
|
||||
} else if (dataset?.type === DatasetType.PRETRAIN_TEXT) {
|
||||
setSelectedCategory("Natural Language Processing");
|
||||
}
|
||||
setSelectedTemplate(null);
|
||||
setFormValues((prev) => ({ ...prev, templateId: "" }));
|
||||
};
|
||||
|
||||
const handleTemplateSelect = (template: Template) => {
|
||||
setSelectedTemplate(template);
|
||||
setFormValues((prev) => ({ ...prev, templateId: template.id }));
|
||||
};
|
||||
|
||||
const handleValuesChange = (_, allValues) => {
|
||||
setFormValues({ ...formValues, ...allValues });
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
const dataset = datasets.find((ds) => ds.id === values.datasetId);
|
||||
const template = mockTemplates.find(
|
||||
(tpl) => tpl.id === values.templateId
|
||||
);
|
||||
if (!dataset) {
|
||||
message.error("请选择数据集");
|
||||
return;
|
||||
}
|
||||
if (!template) {
|
||||
message.error("请选择标注模板");
|
||||
return;
|
||||
}
|
||||
const taskData = {
|
||||
name: values.name,
|
||||
description: values.description,
|
||||
dataset,
|
||||
template,
|
||||
};
|
||||
// onCreateTask(taskData); // 实际创建逻辑
|
||||
message.success("标注任务创建成功");
|
||||
navigate("/data/annotation");
|
||||
} catch (e) {
|
||||
// 校验失败
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveCustomTemplate = (templateData: any) => {
|
||||
setSelectedTemplate(templateData);
|
||||
setFormValues((prev) => ({ ...prev, templateId: templateData.id }));
|
||||
message.success(`自定义模板 "${templateData.name}" 已创建`);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex-overflow-auto">
|
||||
{/* Header */}
|
||||
<div className="flex items-center mb-2">
|
||||
<Link to="/data/annotation">
|
||||
<Button type="text">
|
||||
<ArrowLeft className="w-4 h-4 mr-1" />
|
||||
</Button>
|
||||
</Link>
|
||||
<h1 className="text-xl font-bold bg-clip-text">创建标注任务</h1>
|
||||
</div>
|
||||
|
||||
<div className="flex-overflow-auto bg-white rounded-lg shadow-sm">
|
||||
<div className="flex-1 overflow-y-auto p-6">
|
||||
<Form
|
||||
form={form}
|
||||
initialValues={formValues}
|
||||
onValuesChange={handleValuesChange}
|
||||
layout="vertical"
|
||||
>
|
||||
{/* 基本信息 */}
|
||||
<h2 className="font-medium text-gray-900 text-lg mb-2">基本信息</h2>
|
||||
<Form.Item
|
||||
label="任务名称"
|
||||
name="name"
|
||||
rules={[{ required: true, message: "请输入任务名称" }]}
|
||||
>
|
||||
<Input placeholder="输入任务名称" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="任务描述"
|
||||
name="description"
|
||||
rules={[{ required: true, message: "请输入任务描述" }]}
|
||||
>
|
||||
<TextArea placeholder="详细描述标注任务的要求和目标" rows={3} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="选择数据集"
|
||||
name="datasetId"
|
||||
rules={[{ required: true, message: "请选择数据集" }]}
|
||||
>
|
||||
<Select
|
||||
optionFilterProp="children"
|
||||
value={formValues.datasetId}
|
||||
onChange={handleDatasetSelect}
|
||||
placeholder="请选择数据集"
|
||||
size="large"
|
||||
options={datasets.map((dataset) => ({
|
||||
label: (
|
||||
<div className="flex items-center justify-between gap-3 py-2">
|
||||
<div className="font-medium text-gray-900">
|
||||
{dataset?.icon || <DatabaseOutlined className="mr-2" />}
|
||||
{dataset.name}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500">
|
||||
{dataset?.fileCount} 文件 • {dataset.size}
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
value: dataset.id,
|
||||
}))}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
{/* 模板选择 */}
|
||||
<h2 className="font-medium text-gray-900 text-lg mt-6 mb-2 flex items-center gap-2">
|
||||
模板选择
|
||||
</h2>
|
||||
<Form.Item
|
||||
name="templateId"
|
||||
rules={[{ required: true, message: "请选择标注模板" }]}
|
||||
>
|
||||
<div className="flex">
|
||||
{/* Category Sidebar */}
|
||||
<div className="w-64 pr-6 border-r border-gray-200">
|
||||
<div className="space-y-2">
|
||||
{templateCategories.map((category) => {
|
||||
const isAvailable =
|
||||
selectedDataset?.type === "image"
|
||||
? category === "Computer Vision"
|
||||
: category === "Natural Language Processing";
|
||||
return (
|
||||
<Button
|
||||
key={category}
|
||||
type={
|
||||
selectedCategory === category && isAvailable
|
||||
? "primary"
|
||||
: "default"
|
||||
}
|
||||
block
|
||||
disabled={!isAvailable}
|
||||
onClick={() =>
|
||||
isAvailable && setSelectedCategory(category)
|
||||
}
|
||||
style={{ textAlign: "left", marginBottom: 8 }}
|
||||
>
|
||||
{category}
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
<Button
|
||||
type="dashed"
|
||||
block
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => setShowCustomTemplateDialog(true)}
|
||||
>
|
||||
自定义模板
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{/* Template Grid */}
|
||||
<div className="flex-1 pl-6">
|
||||
<div className="max-h-96 overflow-auto">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{filteredTemplates.map((template) => (
|
||||
<div
|
||||
key={template.id}
|
||||
className={`border rounded-lg cursor-pointer transition-all hover:shadow-md ${
|
||||
formValues.templateId === template.id
|
||||
? "border-blue-500 bg-blue-50"
|
||||
: "border-gray-200"
|
||||
}`}
|
||||
onClick={() => handleTemplateSelect(template)}
|
||||
>
|
||||
{template.preview && (
|
||||
<div className="aspect-video bg-gray-100 rounded-t-lg overflow-hidden">
|
||||
<img
|
||||
src={template.preview || "/placeholder.svg"}
|
||||
alt={template.name}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="p-3">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
{template.icon}
|
||||
<span className="font-medium text-sm">
|
||||
{template.name}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-gray-600">
|
||||
{template.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{/* Custom Template Option */}
|
||||
<div
|
||||
className={`border-2 border-dashed rounded-lg cursor-pointer transition-all hover:border-gray-400 ${
|
||||
selectedTemplate?.isCustom
|
||||
? "border-blue-500 bg-blue-50"
|
||||
: "border-gray-300"
|
||||
}`}
|
||||
onClick={() => setShowCustomTemplateDialog(true)}
|
||||
>
|
||||
<div className="aspect-video bg-gray-50 rounded-t-lg flex items-center justify-center">
|
||||
<PlusOutlined
|
||||
style={{ fontSize: 32, color: "#bbb" }}
|
||||
/>
|
||||
</div>
|
||||
<div className="p-3">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
<PlusOutlined />
|
||||
<span className="font-medium text-sm">
|
||||
自定义模板
|
||||
</span>
|
||||
</div>
|
||||
{selectedTemplate?.isCustom && (
|
||||
<CheckOutlined style={{ color: "#1677ff" }} />
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-gray-600">
|
||||
创建符合特定需求的标注模板
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{selectedTemplate && (
|
||||
<div className="mt-4 p-3 bg-blue-50 rounded-lg">
|
||||
<div className="flex items-center space-x-2">
|
||||
<span
|
||||
className="text-sm font-medium"
|
||||
style={{ color: "#1677ff" }}
|
||||
>
|
||||
已选择模板
|
||||
</span>
|
||||
</div>
|
||||
<p
|
||||
className="text-sm"
|
||||
style={{ color: "#1677ff", marginTop: 4 }}
|
||||
>
|
||||
{selectedTemplate.name} - {selectedTemplate.description}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</div>
|
||||
<div className="flex gap-2 justify-end border-t border-gray-200 p-6">
|
||||
<Button onClick={() => navigate("/data/annotation")}>取消</Button>
|
||||
<Button type="primary" onClick={handleSubmit}>
|
||||
创建任务
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Custom Template Dialog */}
|
||||
<CustomTemplateDialog
|
||||
open={showCustomTemplateDialog}
|
||||
onOpenChange={setShowCustomTemplateDialog}
|
||||
onSaveTemplate={handleSaveCustomTemplate}
|
||||
datasetType={selectedDataset?.type || "image"}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,192 +1,192 @@
|
||||
import { queryDatasetsUsingGet } from "@/pages/DataManagement/dataset.api";
|
||||
import { mapDataset } from "@/pages/DataManagement/dataset.const";
|
||||
import { Button, Form, Input, Modal, Select, message } from "antd";
|
||||
import TextArea from "antd/es/input/TextArea";
|
||||
import { useEffect, useState } from "react";
|
||||
import { createAnnotationTaskUsingPost, queryAnnotationTemplatesUsingGet } from "../../annotation.api";
|
||||
import { Dataset } from "@/pages/DataManagement/dataset.model";
|
||||
import type { AnnotationTemplate } from "../../annotation.model";
|
||||
|
||||
export default function CreateAnnotationTask({
|
||||
open,
|
||||
onClose,
|
||||
onRefresh,
|
||||
}: {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onRefresh: () => void;
|
||||
}) {
|
||||
const [form] = Form.useForm();
|
||||
const [datasets, setDatasets] = useState<Dataset[]>([]);
|
||||
const [templates, setTemplates] = useState<AnnotationTemplate[]>([]);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [nameManuallyEdited, setNameManuallyEdited] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
// Fetch datasets
|
||||
const { data: datasetData } = await queryDatasetsUsingGet({
|
||||
page: 0,
|
||||
pageSize: 1000, // Use camelCase for HTTP params
|
||||
});
|
||||
setDatasets(datasetData.content.map(mapDataset) || []);
|
||||
|
||||
// Fetch templates
|
||||
const templateResponse = await queryAnnotationTemplatesUsingGet({
|
||||
page: 1,
|
||||
size: 100, // Backend max is 100 (template API uses 'size' not 'pageSize')
|
||||
});
|
||||
|
||||
// The API returns: {code, message, data: {content, total, page, ...}}
|
||||
if (templateResponse.code === 200 && templateResponse.data) {
|
||||
const fetchedTemplates = templateResponse.data.content || [];
|
||||
console.log("Fetched templates:", fetchedTemplates);
|
||||
setTemplates(fetchedTemplates);
|
||||
} else {
|
||||
console.error("Failed to fetch templates:", templateResponse);
|
||||
setTemplates([]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching data:", error);
|
||||
setTemplates([]);
|
||||
}
|
||||
};
|
||||
fetchData();
|
||||
}, [open]);
|
||||
|
||||
// Reset form and manual-edit flag when modal opens
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
form.resetFields();
|
||||
setNameManuallyEdited(false);
|
||||
}
|
||||
}, [open, form]);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
setSubmitting(true);
|
||||
// Send templateId instead of labelingConfig
|
||||
const requestData = {
|
||||
name: values.name,
|
||||
description: values.description,
|
||||
datasetId: values.datasetId,
|
||||
templateId: values.templateId,
|
||||
};
|
||||
await createAnnotationTaskUsingPost(requestData);
|
||||
message?.success?.("创建标注任务成功");
|
||||
onClose();
|
||||
onRefresh();
|
||||
} catch (err: any) {
|
||||
console.error("Create annotation task failed", err);
|
||||
const msg = err?.message || err?.data?.message || "创建失败,请稍后重试";
|
||||
(message as any)?.error?.(msg);
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
onCancel={onClose}
|
||||
title="创建标注任务"
|
||||
footer={
|
||||
<>
|
||||
<Button onClick={onClose} disabled={submitting}>
|
||||
取消
|
||||
</Button>
|
||||
<Button type="primary" onClick={handleSubmit} loading={submitting}>
|
||||
确定
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
width={800}
|
||||
>
|
||||
<Form form={form} layout="vertical">
|
||||
{/* 数据集 与 标注工程名称 并排显示(数据集在左) */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Form.Item
|
||||
label="数据集"
|
||||
name="datasetId"
|
||||
rules={[{ required: true, message: "请选择数据集" }]}
|
||||
>
|
||||
<Select
|
||||
placeholder="请选择数据集"
|
||||
options={datasets.map((dataset) => {
|
||||
return {
|
||||
label: (
|
||||
<div className="flex items-center justify-between gap-3 py-2">
|
||||
<div className="flex items-center font-sm text-gray-900">
|
||||
<span className="mr-2">{(dataset as any).icon}</span>
|
||||
<span>{dataset.name}</span>
|
||||
</div>
|
||||
<div className="text-xs text-gray-500">{dataset.size}</div>
|
||||
</div>
|
||||
),
|
||||
value: dataset.id,
|
||||
};
|
||||
})}
|
||||
onChange={(value) => {
|
||||
// 如果用户未手动修改名称,则用数据集名称作为默认任务名
|
||||
if (!nameManuallyEdited) {
|
||||
const ds = datasets.find((d) => d.id === value);
|
||||
if (ds) {
|
||||
form.setFieldsValue({ name: ds.name });
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="标注工程名称"
|
||||
name="name"
|
||||
rules={[{ required: true, message: "请输入任务名称" }]}
|
||||
>
|
||||
<Input
|
||||
placeholder="输入标注工程名称"
|
||||
onChange={() => setNameManuallyEdited(true)}
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
{/* 描述变为可选 */}
|
||||
<Form.Item label="描述" name="description">
|
||||
<TextArea placeholder="(可选)详细描述标注任务的要求和目标" rows={3} />
|
||||
</Form.Item>
|
||||
|
||||
{/* 标注模板选择 */}
|
||||
<Form.Item
|
||||
label="标注模板"
|
||||
name="templateId"
|
||||
rules={[{ required: true, message: "请选择标注模板" }]}
|
||||
>
|
||||
<Select
|
||||
placeholder={templates.length === 0 ? "暂无可用模板,请先创建模板" : "请选择标注模板"}
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
notFoundContent={templates.length === 0 ? "暂无模板,请前往「标注模板」页面创建" : "未找到匹配的模板"}
|
||||
options={templates.map((template) => ({
|
||||
label: template.name,
|
||||
value: template.id,
|
||||
// Add description as subtitle
|
||||
title: template.description,
|
||||
}))}
|
||||
optionRender={(option) => (
|
||||
<div>
|
||||
<div style={{ fontWeight: 500 }}>{option.label}</div>
|
||||
{option.data.title && (
|
||||
<div style={{ fontSize: 12, color: '#999', marginTop: 2 }}>
|
||||
{option.data.title}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
import { queryDatasetsUsingGet } from "@/pages/DataManagement/dataset.api";
|
||||
import { mapDataset } from "@/pages/DataManagement/dataset.const";
|
||||
import { Button, Form, Input, Modal, Select, message } from "antd";
|
||||
import TextArea from "antd/es/input/TextArea";
|
||||
import { useEffect, useState } from "react";
|
||||
import { createAnnotationTaskUsingPost, queryAnnotationTemplatesUsingGet } from "../../annotation.api";
|
||||
import { Dataset } from "@/pages/DataManagement/dataset.model";
|
||||
import type { AnnotationTemplate } from "../../annotation.model";
|
||||
|
||||
export default function CreateAnnotationTask({
|
||||
open,
|
||||
onClose,
|
||||
onRefresh,
|
||||
}: {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onRefresh: () => void;
|
||||
}) {
|
||||
const [form] = Form.useForm();
|
||||
const [datasets, setDatasets] = useState<Dataset[]>([]);
|
||||
const [templates, setTemplates] = useState<AnnotationTemplate[]>([]);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [nameManuallyEdited, setNameManuallyEdited] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
// Fetch datasets
|
||||
const { data: datasetData } = await queryDatasetsUsingGet({
|
||||
page: 0,
|
||||
pageSize: 1000, // Use camelCase for HTTP params
|
||||
});
|
||||
setDatasets(datasetData.content.map(mapDataset) || []);
|
||||
|
||||
// Fetch templates
|
||||
const templateResponse = await queryAnnotationTemplatesUsingGet({
|
||||
page: 1,
|
||||
size: 100, // Backend max is 100 (template API uses 'size' not 'pageSize')
|
||||
});
|
||||
|
||||
// The API returns: {code, message, data: {content, total, page, ...}}
|
||||
if (templateResponse.code === 200 && templateResponse.data) {
|
||||
const fetchedTemplates = templateResponse.data.content || [];
|
||||
console.log("Fetched templates:", fetchedTemplates);
|
||||
setTemplates(fetchedTemplates);
|
||||
} else {
|
||||
console.error("Failed to fetch templates:", templateResponse);
|
||||
setTemplates([]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching data:", error);
|
||||
setTemplates([]);
|
||||
}
|
||||
};
|
||||
fetchData();
|
||||
}, [open]);
|
||||
|
||||
// Reset form and manual-edit flag when modal opens
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
form.resetFields();
|
||||
setNameManuallyEdited(false);
|
||||
}
|
||||
}, [open, form]);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
setSubmitting(true);
|
||||
// Send templateId instead of labelingConfig
|
||||
const requestData = {
|
||||
name: values.name,
|
||||
description: values.description,
|
||||
datasetId: values.datasetId,
|
||||
templateId: values.templateId,
|
||||
};
|
||||
await createAnnotationTaskUsingPost(requestData);
|
||||
message?.success?.("创建标注任务成功");
|
||||
onClose();
|
||||
onRefresh();
|
||||
} catch (err: any) {
|
||||
console.error("Create annotation task failed", err);
|
||||
const msg = err?.message || err?.data?.message || "创建失败,请稍后重试";
|
||||
(message as any)?.error?.(msg);
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
onCancel={onClose}
|
||||
title="创建标注任务"
|
||||
footer={
|
||||
<>
|
||||
<Button onClick={onClose} disabled={submitting}>
|
||||
取消
|
||||
</Button>
|
||||
<Button type="primary" onClick={handleSubmit} loading={submitting}>
|
||||
确定
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
width={800}
|
||||
>
|
||||
<Form form={form} layout="vertical">
|
||||
{/* 数据集 与 标注工程名称 并排显示(数据集在左) */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Form.Item
|
||||
label="数据集"
|
||||
name="datasetId"
|
||||
rules={[{ required: true, message: "请选择数据集" }]}
|
||||
>
|
||||
<Select
|
||||
placeholder="请选择数据集"
|
||||
options={datasets.map((dataset) => {
|
||||
return {
|
||||
label: (
|
||||
<div className="flex items-center justify-between gap-3 py-2">
|
||||
<div className="flex items-center font-sm text-gray-900">
|
||||
<span className="mr-2">{(dataset as any).icon}</span>
|
||||
<span>{dataset.name}</span>
|
||||
</div>
|
||||
<div className="text-xs text-gray-500">{dataset.size}</div>
|
||||
</div>
|
||||
),
|
||||
value: dataset.id,
|
||||
};
|
||||
})}
|
||||
onChange={(value) => {
|
||||
// 如果用户未手动修改名称,则用数据集名称作为默认任务名
|
||||
if (!nameManuallyEdited) {
|
||||
const ds = datasets.find((d) => d.id === value);
|
||||
if (ds) {
|
||||
form.setFieldsValue({ name: ds.name });
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="标注工程名称"
|
||||
name="name"
|
||||
rules={[{ required: true, message: "请输入任务名称" }]}
|
||||
>
|
||||
<Input
|
||||
placeholder="输入标注工程名称"
|
||||
onChange={() => setNameManuallyEdited(true)}
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
{/* 描述变为可选 */}
|
||||
<Form.Item label="描述" name="description">
|
||||
<TextArea placeholder="(可选)详细描述标注任务的要求和目标" rows={3} />
|
||||
</Form.Item>
|
||||
|
||||
{/* 标注模板选择 */}
|
||||
<Form.Item
|
||||
label="标注模板"
|
||||
name="templateId"
|
||||
rules={[{ required: true, message: "请选择标注模板" }]}
|
||||
>
|
||||
<Select
|
||||
placeholder={templates.length === 0 ? "暂无可用模板,请先创建模板" : "请选择标注模板"}
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
notFoundContent={templates.length === 0 ? "暂无模板,请前往「标注模板」页面创建" : "未找到匹配的模板"}
|
||||
options={templates.map((template) => ({
|
||||
label: template.name,
|
||||
value: template.id,
|
||||
// Add description as subtitle
|
||||
title: template.description,
|
||||
}))}
|
||||
optionRender={(option) => (
|
||||
<div>
|
||||
<div style={{ fontWeight: 500 }}>{option.label}</div>
|
||||
{option.data.title && (
|
||||
<div style={{ fontSize: 12, color: '#999', marginTop: 2 }}>
|
||||
{option.data.title}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,195 +1,195 @@
|
||||
import { queryDatasetsUsingGet } from "@/pages/DataManagement/dataset.api";
|
||||
import { mapDataset } from "@/pages/DataManagement/dataset.const";
|
||||
import { Button, Form, Input, Modal, Select, message } from "antd";
|
||||
import TextArea from "antd/es/input/TextArea";
|
||||
import { useEffect, useState } from "react";
|
||||
import { createAnnotationTaskUsingPost, queryAnnotationTemplatesUsingGet } from "../../annotation.api";
|
||||
import { Dataset } from "@/pages/DataManagement/dataset.model";
|
||||
import type { AnnotationTemplate } from "../../annotation.model";
|
||||
|
||||
export default function CreateAnnotationTask({
|
||||
open,
|
||||
onClose,
|
||||
onRefresh,
|
||||
}: {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onRefresh: () => void;
|
||||
}) {
|
||||
const [form] = Form.useForm();
|
||||
const [datasets, setDatasets] = useState<Dataset[]>([]);
|
||||
const [templates, setTemplates] = useState<AnnotationTemplate[]>([]);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [nameManuallyEdited, setNameManuallyEdited] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
// Fetch datasets
|
||||
const { data: datasetData } = await queryDatasetsUsingGet({
|
||||
page: 0,
|
||||
pageSize: 1000, // Use camelCase for HTTP params
|
||||
});
|
||||
setDatasets(datasetData.content.map(mapDataset) || []);
|
||||
|
||||
// Fetch templates
|
||||
const templateResponse = await queryAnnotationTemplatesUsingGet({
|
||||
page: 1,
|
||||
size: 100, // Backend max is 100 (template API uses 'size' not 'pageSize')
|
||||
});
|
||||
|
||||
// The API returns: {code, message, data: {content, total, page, ...}}
|
||||
if (templateResponse.code === 200 && templateResponse.data) {
|
||||
const fetchedTemplates = templateResponse.data.content || [];
|
||||
console.log("Fetched templates:", fetchedTemplates);
|
||||
setTemplates(fetchedTemplates);
|
||||
} else {
|
||||
console.error("Failed to fetch templates:", templateResponse);
|
||||
setTemplates([]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching data:", error);
|
||||
setTemplates([]);
|
||||
}
|
||||
};
|
||||
fetchData();
|
||||
}, [open]);
|
||||
|
||||
// Reset form and manual-edit flag when modal opens
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
form.resetFields();
|
||||
setNameManuallyEdited(false);
|
||||
}
|
||||
}, [open, form]);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
setSubmitting(true);
|
||||
|
||||
// Send templateId instead of labelingConfig
|
||||
const requestData = {
|
||||
name: values.name,
|
||||
description: values.description,
|
||||
datasetId: values.datasetId,
|
||||
templateId: values.templateId,
|
||||
};
|
||||
|
||||
await createAnnotationTaskUsingPost(requestData);
|
||||
message?.success?.("创建标注任务成功");
|
||||
onClose();
|
||||
onRefresh();
|
||||
} catch (err: any) {
|
||||
console.error("Create annotation task failed", err);
|
||||
const msg = err?.message || err?.data?.message || "创建失败,请稍后重试";
|
||||
(message as any)?.error?.(msg);
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
onCancel={onClose}
|
||||
title="创建标注任务"
|
||||
footer={
|
||||
<>
|
||||
<Button onClick={onClose} disabled={submitting}>
|
||||
取消
|
||||
</Button>
|
||||
<Button type="primary" onClick={handleSubmit} loading={submitting}>
|
||||
确定
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
width={800}
|
||||
>
|
||||
<Form form={form} layout="vertical">
|
||||
{/* 数据集 与 标注工程名称 并排显示(数据集在左) */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Form.Item
|
||||
label="数据集"
|
||||
name="datasetId"
|
||||
rules={[{ required: true, message: "请选择数据集" }]}
|
||||
>
|
||||
<Select
|
||||
placeholder="请选择数据集"
|
||||
options={datasets.map((dataset) => {
|
||||
return {
|
||||
label: (
|
||||
<div className="flex items-center justify-between gap-3 py-2">
|
||||
<div className="flex items-center font-sm text-gray-900">
|
||||
<span className="mr-2">{(dataset as any).icon}</span>
|
||||
<span>{dataset.name}</span>
|
||||
</div>
|
||||
<div className="text-xs text-gray-500">{dataset.size}</div>
|
||||
</div>
|
||||
),
|
||||
value: dataset.id,
|
||||
};
|
||||
})}
|
||||
onChange={(value) => {
|
||||
// 如果用户未手动修改名称,则用数据集名称作为默认任务名
|
||||
if (!nameManuallyEdited) {
|
||||
const ds = datasets.find((d) => d.id === value);
|
||||
if (ds) {
|
||||
form.setFieldsValue({ name: ds.name });
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="标注工程名称"
|
||||
name="name"
|
||||
rules={[{ required: true, message: "请输入任务名称" }]}
|
||||
>
|
||||
<Input
|
||||
placeholder="输入标注工程名称"
|
||||
onChange={() => setNameManuallyEdited(true)}
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
{/* 描述变为可选 */}
|
||||
<Form.Item label="描述" name="description">
|
||||
<TextArea placeholder="(可选)详细描述标注任务的要求和目标" rows={3} />
|
||||
</Form.Item>
|
||||
|
||||
{/* 标注模板选择 */}
|
||||
<Form.Item
|
||||
label="标注模板"
|
||||
name="templateId"
|
||||
rules={[{ required: true, message: "请选择标注模板" }]}
|
||||
>
|
||||
<Select
|
||||
placeholder={templates.length === 0 ? "暂无可用模板,请先创建模板" : "请选择标注模板"}
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
notFoundContent={templates.length === 0 ? "暂无模板,请前往「标注模板」页面创建" : "未找到匹配的模板"}
|
||||
options={templates.map((template) => ({
|
||||
label: template.name,
|
||||
value: template.id,
|
||||
// Add description as subtitle
|
||||
title: template.description,
|
||||
}))}
|
||||
optionRender={(option) => (
|
||||
<div>
|
||||
<div style={{ fontWeight: 500 }}>{option.label}</div>
|
||||
{option.data.title && (
|
||||
<div style={{ fontSize: 12, color: '#999', marginTop: 2 }}>
|
||||
{option.data.title}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
import { queryDatasetsUsingGet } from "@/pages/DataManagement/dataset.api";
|
||||
import { mapDataset } from "@/pages/DataManagement/dataset.const";
|
||||
import { Button, Form, Input, Modal, Select, message } from "antd";
|
||||
import TextArea from "antd/es/input/TextArea";
|
||||
import { useEffect, useState } from "react";
|
||||
import { createAnnotationTaskUsingPost, queryAnnotationTemplatesUsingGet } from "../../annotation.api";
|
||||
import { Dataset } from "@/pages/DataManagement/dataset.model";
|
||||
import type { AnnotationTemplate } from "../../annotation.model";
|
||||
|
||||
export default function CreateAnnotationTask({
|
||||
open,
|
||||
onClose,
|
||||
onRefresh,
|
||||
}: {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onRefresh: () => void;
|
||||
}) {
|
||||
const [form] = Form.useForm();
|
||||
const [datasets, setDatasets] = useState<Dataset[]>([]);
|
||||
const [templates, setTemplates] = useState<AnnotationTemplate[]>([]);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [nameManuallyEdited, setNameManuallyEdited] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
// Fetch datasets
|
||||
const { data: datasetData } = await queryDatasetsUsingGet({
|
||||
page: 0,
|
||||
pageSize: 1000, // Use camelCase for HTTP params
|
||||
});
|
||||
setDatasets(datasetData.content.map(mapDataset) || []);
|
||||
|
||||
// Fetch templates
|
||||
const templateResponse = await queryAnnotationTemplatesUsingGet({
|
||||
page: 1,
|
||||
size: 100, // Backend max is 100 (template API uses 'size' not 'pageSize')
|
||||
});
|
||||
|
||||
// The API returns: {code, message, data: {content, total, page, ...}}
|
||||
if (templateResponse.code === 200 && templateResponse.data) {
|
||||
const fetchedTemplates = templateResponse.data.content || [];
|
||||
console.log("Fetched templates:", fetchedTemplates);
|
||||
setTemplates(fetchedTemplates);
|
||||
} else {
|
||||
console.error("Failed to fetch templates:", templateResponse);
|
||||
setTemplates([]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching data:", error);
|
||||
setTemplates([]);
|
||||
}
|
||||
};
|
||||
fetchData();
|
||||
}, [open]);
|
||||
|
||||
// Reset form and manual-edit flag when modal opens
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
form.resetFields();
|
||||
setNameManuallyEdited(false);
|
||||
}
|
||||
}, [open, form]);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
setSubmitting(true);
|
||||
|
||||
// Send templateId instead of labelingConfig
|
||||
const requestData = {
|
||||
name: values.name,
|
||||
description: values.description,
|
||||
datasetId: values.datasetId,
|
||||
templateId: values.templateId,
|
||||
};
|
||||
|
||||
await createAnnotationTaskUsingPost(requestData);
|
||||
message?.success?.("创建标注任务成功");
|
||||
onClose();
|
||||
onRefresh();
|
||||
} catch (err: any) {
|
||||
console.error("Create annotation task failed", err);
|
||||
const msg = err?.message || err?.data?.message || "创建失败,请稍后重试";
|
||||
(message as any)?.error?.(msg);
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
onCancel={onClose}
|
||||
title="创建标注任务"
|
||||
footer={
|
||||
<>
|
||||
<Button onClick={onClose} disabled={submitting}>
|
||||
取消
|
||||
</Button>
|
||||
<Button type="primary" onClick={handleSubmit} loading={submitting}>
|
||||
确定
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
width={800}
|
||||
>
|
||||
<Form form={form} layout="vertical">
|
||||
{/* 数据集 与 标注工程名称 并排显示(数据集在左) */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Form.Item
|
||||
label="数据集"
|
||||
name="datasetId"
|
||||
rules={[{ required: true, message: "请选择数据集" }]}
|
||||
>
|
||||
<Select
|
||||
placeholder="请选择数据集"
|
||||
options={datasets.map((dataset) => {
|
||||
return {
|
||||
label: (
|
||||
<div className="flex items-center justify-between gap-3 py-2">
|
||||
<div className="flex items-center font-sm text-gray-900">
|
||||
<span className="mr-2">{(dataset as any).icon}</span>
|
||||
<span>{dataset.name}</span>
|
||||
</div>
|
||||
<div className="text-xs text-gray-500">{dataset.size}</div>
|
||||
</div>
|
||||
),
|
||||
value: dataset.id,
|
||||
};
|
||||
})}
|
||||
onChange={(value) => {
|
||||
// 如果用户未手动修改名称,则用数据集名称作为默认任务名
|
||||
if (!nameManuallyEdited) {
|
||||
const ds = datasets.find((d) => d.id === value);
|
||||
if (ds) {
|
||||
form.setFieldsValue({ name: ds.name });
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="标注工程名称"
|
||||
name="name"
|
||||
rules={[{ required: true, message: "请输入任务名称" }]}
|
||||
>
|
||||
<Input
|
||||
placeholder="输入标注工程名称"
|
||||
onChange={() => setNameManuallyEdited(true)}
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
{/* 描述变为可选 */}
|
||||
<Form.Item label="描述" name="description">
|
||||
<TextArea placeholder="(可选)详细描述标注任务的要求和目标" rows={3} />
|
||||
</Form.Item>
|
||||
|
||||
{/* 标注模板选择 */}
|
||||
<Form.Item
|
||||
label="标注模板"
|
||||
name="templateId"
|
||||
rules={[{ required: true, message: "请选择标注模板" }]}
|
||||
>
|
||||
<Select
|
||||
placeholder={templates.length === 0 ? "暂无可用模板,请先创建模板" : "请选择标注模板"}
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
notFoundContent={templates.length === 0 ? "暂无模板,请前往「标注模板」页面创建" : "未找到匹配的模板"}
|
||||
options={templates.map((template) => ({
|
||||
label: template.name,
|
||||
value: template.id,
|
||||
// Add description as subtitle
|
||||
title: template.description,
|
||||
}))}
|
||||
optionRender={(option) => (
|
||||
<div>
|
||||
<div style={{ fontWeight: 500 }}>{option.label}</div>
|
||||
{option.data.title && (
|
||||
<div style={{ fontSize: 12, color: '#999', marginTop: 2 }}>
|
||||
{option.data.title}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,225 +1,225 @@
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Modal,
|
||||
Input,
|
||||
Card,
|
||||
message,
|
||||
Divider,
|
||||
Radio,
|
||||
Form,
|
||||
} from "antd";
|
||||
import {
|
||||
AppstoreOutlined,
|
||||
BorderOutlined,
|
||||
DotChartOutlined,
|
||||
EditOutlined,
|
||||
CheckSquareOutlined,
|
||||
BarsOutlined,
|
||||
DeploymentUnitOutlined,
|
||||
} from "@ant-design/icons";
|
||||
|
||||
interface CustomTemplateDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onSaveTemplate: (templateData: any) => void;
|
||||
datasetType: "text" | "image";
|
||||
}
|
||||
|
||||
const { TextArea } = Input;
|
||||
|
||||
const defaultImageTemplate = `<View style="display: flex; flex-direction: column; height: 100vh; overflow: auto;">
|
||||
<View style="display: flex; height: 100%; gap: 10px;">
|
||||
<View style="height: 100%; width: 85%; display: flex; flex-direction: column; gap: 5px;">
|
||||
<Header value="WSI图像预览" />
|
||||
<View style="min-height: 100%;">
|
||||
<Image name="image" value="$image" zoom="true" />
|
||||
</View>
|
||||
</View>
|
||||
<View style="height: 100%; width: auto;">
|
||||
<View style="width: auto; display: flex;">
|
||||
<Text name="case_id_title" toName="image" value="病例号: $case_id" />
|
||||
</View>
|
||||
<Text name="part_title" toName="image" value="取材部位: $part" />
|
||||
<Header value="标注" />
|
||||
<View style="display: flex; gap: 5px;">
|
||||
<View>
|
||||
<Text name="cancer_or_not_title" value="是否有肿瘤" />
|
||||
<Choices name="cancer_or_not" toName="image">
|
||||
<Choice value="是" alias="1" />
|
||||
<Choice value="否" alias="0" />
|
||||
</Choices>
|
||||
<Text name="remark_title" value="备注" />
|
||||
<TextArea name="remark" toName="image" editable="true"/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>`;
|
||||
|
||||
const defaultTextTemplate = `<View style="display: flex; flex-direction: column; height: 100vh;">
|
||||
<Header value="文本标注界面" />
|
||||
<View style="display: flex; height: 100%; gap: 10px;">
|
||||
<View style="flex: 1; padding: 10px;">
|
||||
<Text name="content" value="$text" />
|
||||
<Labels name="label" toName="content">
|
||||
<Label value="正面" background="green" />
|
||||
<Label value="负面" background="red" />
|
||||
<Label value="中性" background="gray" />
|
||||
</Labels>
|
||||
</View>
|
||||
<View style="width: 300px; padding: 10px; border-left: 1px solid #ccc;">
|
||||
<Header value="标注选项" />
|
||||
<Text name="sentiment_title" value="情感分类" />
|
||||
<Choices name="sentiment" toName="content">
|
||||
<Choice value="正面" />
|
||||
<Choice value="负面" />
|
||||
<Choice value="中性" />
|
||||
</Choices>
|
||||
<Text name="confidence_title" value="置信度" />
|
||||
<Rating name="confidence" toName="content" maxRating="5" />
|
||||
<Text name="comment_title" value="备注" />
|
||||
<TextArea name="comment" toName="content" placeholder="添加备注..." />
|
||||
</View>
|
||||
</View>
|
||||
</View>`;
|
||||
|
||||
const annotationTools = [
|
||||
{ id: "rectangle", label: "矩形框", icon: <BorderOutlined />, type: "image" },
|
||||
{
|
||||
id: "polygon",
|
||||
label: "多边形",
|
||||
icon: <DeploymentUnitOutlined />,
|
||||
type: "image",
|
||||
},
|
||||
{ id: "circle", label: "圆形", icon: <DotChartOutlined />, type: "image" },
|
||||
{ id: "point", label: "关键点", icon: <AppstoreOutlined />, type: "image" },
|
||||
{ id: "text", label: "文本", icon: <EditOutlined />, type: "both" },
|
||||
{ id: "choices", label: "选择题", icon: <BarsOutlined />, type: "both" },
|
||||
{
|
||||
id: "checkbox",
|
||||
label: "多选框",
|
||||
icon: <CheckSquareOutlined />,
|
||||
type: "both",
|
||||
},
|
||||
{ id: "textarea", label: "文本域", icon: <BarsOutlined />, type: "both" },
|
||||
];
|
||||
|
||||
export default function CustomTemplateDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
onSaveTemplate,
|
||||
datasetType,
|
||||
}: CustomTemplateDialogProps) {
|
||||
const [templateName, setTemplateName] = useState("");
|
||||
const [templateDescription, setTemplateDescription] = useState("");
|
||||
const [templateCode, setTemplateCode] = useState(
|
||||
datasetType === "image" ? defaultImageTemplate : defaultTextTemplate
|
||||
);
|
||||
|
||||
const handleSave = () => {
|
||||
if (!templateName.trim()) {
|
||||
message.error("请输入模板名称");
|
||||
return;
|
||||
}
|
||||
if (!templateCode.trim()) {
|
||||
message.error("请输入模板代码");
|
||||
return;
|
||||
}
|
||||
const templateData = {
|
||||
id: `custom-${Date.now()}`,
|
||||
name: templateName,
|
||||
description: templateDescription,
|
||||
code: templateCode,
|
||||
type: datasetType,
|
||||
isCustom: true,
|
||||
};
|
||||
onSaveTemplate(templateData);
|
||||
onOpenChange(false);
|
||||
message.success("自定义模板已保存");
|
||||
setTemplateName("");
|
||||
setTemplateDescription("");
|
||||
setTemplateCode(
|
||||
datasetType === "image" ? defaultImageTemplate : defaultTextTemplate
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
onCancel={() => onOpenChange(false)}
|
||||
okText={"保存模板"}
|
||||
onOk={handleSave}
|
||||
width={1200}
|
||||
className="max-h-[80vh] overflow-auto"
|
||||
title="自定义标注模板"
|
||||
>
|
||||
<div className="flex min-h-[500px]">
|
||||
<div className="flex-1 pl-6">
|
||||
<Form layout="vertical">
|
||||
<Form.Item label="模板名称 *" required>
|
||||
<Input
|
||||
placeholder="输入模板名称"
|
||||
value={templateName}
|
||||
onChange={(e) => setTemplateName(e.target.value)}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="模板描述">
|
||||
<Input
|
||||
placeholder="输入模板描述"
|
||||
value={templateDescription}
|
||||
onChange={(e) => setTemplateDescription(e.target.value)}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
<div className="flex gap-6">
|
||||
<div className="flex-1">
|
||||
<div className="mb-2 font-medium">代码</div>
|
||||
<Card>
|
||||
<TextArea
|
||||
rows={20}
|
||||
value={templateCode}
|
||||
onChange={(e) => setTemplateCode(e.target.value)}
|
||||
placeholder="输入模板代码"
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
<div className="w-96 border-l border-gray-100 pl-6">
|
||||
<div className="mb-2 font-medium">预览</div>
|
||||
<Card
|
||||
cover={
|
||||
<img
|
||||
alt="预览图像"
|
||||
src="https://hebbkx1anhila5yf.public.blob.vercel-storage.com/img_v3_02oi_9b855efe-ce37-4387-a845-d8ef9aaa1a8g.jpg-GhkhlenJlzOQLSDqyBm2iaC6jbv7VA.jpeg"
|
||||
className="object-cover h-48"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div className="mb-2">
|
||||
<span className="text-gray-500">病例号:</span>
|
||||
<span>undefined</span>
|
||||
</div>
|
||||
<div className="mb-2">
|
||||
<span className="text-gray-500">取材部位:</span>
|
||||
<span>undefined</span>
|
||||
</div>
|
||||
<Divider />
|
||||
<div>
|
||||
<div className="font-medium mb-2">标注</div>
|
||||
<div className="mb-2 text-gray-500">是否有肿瘤</div>
|
||||
<Radio.Group>
|
||||
<Radio value="1">是[1]</Radio>
|
||||
<Radio value="0">否[2]</Radio>
|
||||
</Radio.Group>
|
||||
<div className="mt-4">
|
||||
<div className="text-gray-500 mb-1">备注</div>
|
||||
<TextArea rows={3} placeholder="添加备注..." />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Modal,
|
||||
Input,
|
||||
Card,
|
||||
message,
|
||||
Divider,
|
||||
Radio,
|
||||
Form,
|
||||
} from "antd";
|
||||
import {
|
||||
AppstoreOutlined,
|
||||
BorderOutlined,
|
||||
DotChartOutlined,
|
||||
EditOutlined,
|
||||
CheckSquareOutlined,
|
||||
BarsOutlined,
|
||||
DeploymentUnitOutlined,
|
||||
} from "@ant-design/icons";
|
||||
|
||||
interface CustomTemplateDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onSaveTemplate: (templateData: any) => void;
|
||||
datasetType: "text" | "image";
|
||||
}
|
||||
|
||||
const { TextArea } = Input;
|
||||
|
||||
const defaultImageTemplate = `<View style="display: flex; flex-direction: column; height: 100vh; overflow: auto;">
|
||||
<View style="display: flex; height: 100%; gap: 10px;">
|
||||
<View style="height: 100%; width: 85%; display: flex; flex-direction: column; gap: 5px;">
|
||||
<Header value="WSI图像预览" />
|
||||
<View style="min-height: 100%;">
|
||||
<Image name="image" value="$image" zoom="true" />
|
||||
</View>
|
||||
</View>
|
||||
<View style="height: 100%; width: auto;">
|
||||
<View style="width: auto; display: flex;">
|
||||
<Text name="case_id_title" toName="image" value="病例号: $case_id" />
|
||||
</View>
|
||||
<Text name="part_title" toName="image" value="取材部位: $part" />
|
||||
<Header value="标注" />
|
||||
<View style="display: flex; gap: 5px;">
|
||||
<View>
|
||||
<Text name="cancer_or_not_title" value="是否有肿瘤" />
|
||||
<Choices name="cancer_or_not" toName="image">
|
||||
<Choice value="是" alias="1" />
|
||||
<Choice value="否" alias="0" />
|
||||
</Choices>
|
||||
<Text name="remark_title" value="备注" />
|
||||
<TextArea name="remark" toName="image" editable="true"/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>`;
|
||||
|
||||
const defaultTextTemplate = `<View style="display: flex; flex-direction: column; height: 100vh;">
|
||||
<Header value="文本标注界面" />
|
||||
<View style="display: flex; height: 100%; gap: 10px;">
|
||||
<View style="flex: 1; padding: 10px;">
|
||||
<Text name="content" value="$text" />
|
||||
<Labels name="label" toName="content">
|
||||
<Label value="正面" background="green" />
|
||||
<Label value="负面" background="red" />
|
||||
<Label value="中性" background="gray" />
|
||||
</Labels>
|
||||
</View>
|
||||
<View style="width: 300px; padding: 10px; border-left: 1px solid #ccc;">
|
||||
<Header value="标注选项" />
|
||||
<Text name="sentiment_title" value="情感分类" />
|
||||
<Choices name="sentiment" toName="content">
|
||||
<Choice value="正面" />
|
||||
<Choice value="负面" />
|
||||
<Choice value="中性" />
|
||||
</Choices>
|
||||
<Text name="confidence_title" value="置信度" />
|
||||
<Rating name="confidence" toName="content" maxRating="5" />
|
||||
<Text name="comment_title" value="备注" />
|
||||
<TextArea name="comment" toName="content" placeholder="添加备注..." />
|
||||
</View>
|
||||
</View>
|
||||
</View>`;
|
||||
|
||||
const annotationTools = [
|
||||
{ id: "rectangle", label: "矩形框", icon: <BorderOutlined />, type: "image" },
|
||||
{
|
||||
id: "polygon",
|
||||
label: "多边形",
|
||||
icon: <DeploymentUnitOutlined />,
|
||||
type: "image",
|
||||
},
|
||||
{ id: "circle", label: "圆形", icon: <DotChartOutlined />, type: "image" },
|
||||
{ id: "point", label: "关键点", icon: <AppstoreOutlined />, type: "image" },
|
||||
{ id: "text", label: "文本", icon: <EditOutlined />, type: "both" },
|
||||
{ id: "choices", label: "选择题", icon: <BarsOutlined />, type: "both" },
|
||||
{
|
||||
id: "checkbox",
|
||||
label: "多选框",
|
||||
icon: <CheckSquareOutlined />,
|
||||
type: "both",
|
||||
},
|
||||
{ id: "textarea", label: "文本域", icon: <BarsOutlined />, type: "both" },
|
||||
];
|
||||
|
||||
export default function CustomTemplateDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
onSaveTemplate,
|
||||
datasetType,
|
||||
}: CustomTemplateDialogProps) {
|
||||
const [templateName, setTemplateName] = useState("");
|
||||
const [templateDescription, setTemplateDescription] = useState("");
|
||||
const [templateCode, setTemplateCode] = useState(
|
||||
datasetType === "image" ? defaultImageTemplate : defaultTextTemplate
|
||||
);
|
||||
|
||||
const handleSave = () => {
|
||||
if (!templateName.trim()) {
|
||||
message.error("请输入模板名称");
|
||||
return;
|
||||
}
|
||||
if (!templateCode.trim()) {
|
||||
message.error("请输入模板代码");
|
||||
return;
|
||||
}
|
||||
const templateData = {
|
||||
id: `custom-${Date.now()}`,
|
||||
name: templateName,
|
||||
description: templateDescription,
|
||||
code: templateCode,
|
||||
type: datasetType,
|
||||
isCustom: true,
|
||||
};
|
||||
onSaveTemplate(templateData);
|
||||
onOpenChange(false);
|
||||
message.success("自定义模板已保存");
|
||||
setTemplateName("");
|
||||
setTemplateDescription("");
|
||||
setTemplateCode(
|
||||
datasetType === "image" ? defaultImageTemplate : defaultTextTemplate
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
onCancel={() => onOpenChange(false)}
|
||||
okText={"保存模板"}
|
||||
onOk={handleSave}
|
||||
width={1200}
|
||||
className="max-h-[80vh] overflow-auto"
|
||||
title="自定义标注模板"
|
||||
>
|
||||
<div className="flex min-h-[500px]">
|
||||
<div className="flex-1 pl-6">
|
||||
<Form layout="vertical">
|
||||
<Form.Item label="模板名称 *" required>
|
||||
<Input
|
||||
placeholder="输入模板名称"
|
||||
value={templateName}
|
||||
onChange={(e) => setTemplateName(e.target.value)}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="模板描述">
|
||||
<Input
|
||||
placeholder="输入模板描述"
|
||||
value={templateDescription}
|
||||
onChange={(e) => setTemplateDescription(e.target.value)}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
<div className="flex gap-6">
|
||||
<div className="flex-1">
|
||||
<div className="mb-2 font-medium">代码</div>
|
||||
<Card>
|
||||
<TextArea
|
||||
rows={20}
|
||||
value={templateCode}
|
||||
onChange={(e) => setTemplateCode(e.target.value)}
|
||||
placeholder="输入模板代码"
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
<div className="w-96 border-l border-gray-100 pl-6">
|
||||
<div className="mb-2 font-medium">预览</div>
|
||||
<Card
|
||||
cover={
|
||||
<img
|
||||
alt="预览图像"
|
||||
src="https://hebbkx1anhila5yf.public.blob.vercel-storage.com/img_v3_02oi_9b855efe-ce37-4387-a845-d8ef9aaa1a8g.jpg-GhkhlenJlzOQLSDqyBm2iaC6jbv7VA.jpeg"
|
||||
className="object-cover h-48"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div className="mb-2">
|
||||
<span className="text-gray-500">病例号:</span>
|
||||
<span>undefined</span>
|
||||
</div>
|
||||
<div className="mb-2">
|
||||
<span className="text-gray-500">取材部位:</span>
|
||||
<span>undefined</span>
|
||||
</div>
|
||||
<Divider />
|
||||
<div>
|
||||
<div className="font-medium mb-2">标注</div>
|
||||
<div className="mb-2 text-gray-500">是否有肿瘤</div>
|
||||
<Radio.Group>
|
||||
<Radio value="1">是[1]</Radio>
|
||||
<Radio value="0">否[2]</Radio>
|
||||
</Radio.Group>
|
||||
<div className="mt-4">
|
||||
<div className="text-gray-500 mb-1">备注</div>
|
||||
<TextArea rows={3} placeholder="添加备注..." />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,187 +1,187 @@
|
||||
import { Button, Card, Input, InputNumber, Popconfirm, Select, Switch, Tooltip } from "antd";
|
||||
import { PlusOutlined, DeleteOutlined } from "@ant-design/icons";
|
||||
import { useState, useImperativeHandle, forwardRef } from "react";
|
||||
|
||||
type LabelType = "string" | "number" | "enum";
|
||||
|
||||
type LabelItem = {
|
||||
id: string;
|
||||
name: string;
|
||||
type: LabelType;
|
||||
required?: boolean;
|
||||
// for enum: values; for number: min/max
|
||||
values?: string[];
|
||||
min?: number | null;
|
||||
max?: number | null;
|
||||
step?: number | null;
|
||||
};
|
||||
|
||||
type LabelingConfigEditorProps = {
|
||||
initial?: any;
|
||||
onGenerate: (config: any) => void;
|
||||
hideFooter?: boolean;
|
||||
};
|
||||
|
||||
export default forwardRef<any, LabelingConfigEditorProps>(function LabelingConfigEditor(
|
||||
{ initial, onGenerate, hideFooter }: LabelingConfigEditorProps,
|
||||
ref: any
|
||||
) {
|
||||
const [labels, setLabels] = useState<LabelItem[]>(() => {
|
||||
if (initial && initial.labels && Array.isArray(initial.labels)) {
|
||||
return initial.labels.map((l: any, idx: number) => ({
|
||||
id: `${Date.now()}-${idx}`,
|
||||
name: l.name || "",
|
||||
type: l.type || "string",
|
||||
required: !!l.required,
|
||||
values: l.values || (l.type === "enum" ? [] : undefined),
|
||||
min: l.min ?? null,
|
||||
max: l.max ?? null,
|
||||
}));
|
||||
}
|
||||
return [];
|
||||
});
|
||||
|
||||
const addLabel = () => {
|
||||
setLabels((s) => [
|
||||
...s,
|
||||
{ id: `${Date.now()}-${Math.random()}`, name: "", type: "string", required: false, step: null },
|
||||
]);
|
||||
};
|
||||
|
||||
const updateLabel = (id: string, patch: Partial<LabelItem>) => {
|
||||
setLabels((s) => s.map((l) => (l.id === id ? { ...l, ...patch } : l)));
|
||||
};
|
||||
|
||||
const removeLabel = (id: string) => {
|
||||
setLabels((s) => s.filter((l) => l.id !== id));
|
||||
};
|
||||
|
||||
const generate = () => {
|
||||
// basic validation: label name non-empty
|
||||
for (const l of labels) {
|
||||
if (!l.name || l.name.trim() === "") {
|
||||
// focus not available here, just abort
|
||||
// Could show a more friendly UI; keep simple for now
|
||||
// eslint-disable-next-line no-alert
|
||||
alert("请为所有标签填写名称");
|
||||
return;
|
||||
}
|
||||
if (l.type === "enum") {
|
||||
if (!l.values || l.values.length === 0) {
|
||||
alert(`枚举标签 ${l.name} 需要至少一个取值`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (l.type === "number") {
|
||||
// validate min/max
|
||||
if (l.min != null && l.max != null && l.min > l.max) {
|
||||
alert(`数值标签 ${l.name} 的最小值不能大于最大值`);
|
||||
return;
|
||||
}
|
||||
// validate step
|
||||
if (l.step != null && (!(typeof l.step === "number") || l.step <= 0)) {
|
||||
alert(`数值标签 ${l.name} 的步长必须为大于 0 的数值`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const config = {
|
||||
labels: labels.map((l) => {
|
||||
const item: any = { name: l.name, type: l.type, required: !!l.required };
|
||||
if (l.type === "enum") item.values = l.values || [];
|
||||
if (l.type === "number") {
|
||||
if (l.min != null) item.min = l.min;
|
||||
if (l.max != null) item.max = l.max;
|
||||
}
|
||||
return item;
|
||||
}),
|
||||
};
|
||||
|
||||
onGenerate(config);
|
||||
};
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
addLabel,
|
||||
generate,
|
||||
}));
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
|
||||
{labels.map((label) => (
|
||||
<Card size="small" key={label.id} styles={{ body: { padding: 10 } }}>
|
||||
<div style={{ display: "flex", flexWrap: "wrap", gap: 8, alignItems: "center" }}>
|
||||
<Input
|
||||
placeholder="标签名称"
|
||||
value={label.name}
|
||||
onChange={(e) => updateLabel(label.id, { name: e.target.value })}
|
||||
style={{ flex: "1 1 160px", minWidth: 120 }}
|
||||
/>
|
||||
<Select
|
||||
value={label.type}
|
||||
onChange={(v) => updateLabel(label.id, { type: v as LabelType })}
|
||||
options={[{ label: "文本", value: "string" }, { label: "数值", value: "number" }, { label: "枚举", value: "enum" }]}
|
||||
style={{ width: 120, flex: "0 0 120px" }}
|
||||
/>
|
||||
|
||||
{label.type === "enum" && (
|
||||
<Input.TextArea
|
||||
placeholder="每行一个枚举值,按回车换行"
|
||||
value={(label.values || []).join("\n")}
|
||||
onChange={(e) => updateLabel(label.id, { values: e.target.value.split(/\r?\n/).map((s) => s.trim()).filter(Boolean) })}
|
||||
onKeyDown={(e) => {
|
||||
// Prevent parent handlers (like Form submit or modal shortcuts) from intercepting Enter
|
||||
e.stopPropagation();
|
||||
}}
|
||||
rows={3}
|
||||
style={{ flex: "1 1 220px", minWidth: 160, width: "100%", resize: "vertical" }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{label.type === "number" && (
|
||||
<div style={{ display: "flex", gap: 8, alignItems: "center", flex: "0 0 auto" }}>
|
||||
<Tooltip title="最小值">
|
||||
<InputNumber value={label.min ?? null} onChange={(v) => updateLabel(label.id, { min: v ?? null })} placeholder="min" />
|
||||
</Tooltip>
|
||||
<Tooltip title="最大值">
|
||||
<InputNumber value={label.max ?? null} onChange={(v) => updateLabel(label.id, { max: v ?? null })} placeholder="max" />
|
||||
</Tooltip>
|
||||
<Tooltip title="步长 (step)">
|
||||
<InputNumber value={label.step ?? null} onChange={(v) => updateLabel(label.id, { step: v ?? null })} placeholder="step" min={0} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 8, marginLeft: "auto" }}>
|
||||
<span style={{ fontSize: 12, color: "rgba(0,0,0,0.65)" }}>必填</span>
|
||||
<Switch checked={!!label.required} onChange={(v) => updateLabel(label.id, { required: v })} />
|
||||
<Popconfirm title="确认删除该标签?" onConfirm={() => removeLabel(label.id)}>
|
||||
<Button type="text" icon={<DeleteOutlined />} />
|
||||
</Popconfirm>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ marginTop: 8, color: "rgba(0,0,0,0.45)", fontSize: 12 }}>
|
||||
{label.type === "string" && <span>类型:文本</span>}
|
||||
{label.type === "number" && <span>类型:数值,支持 min / max / step</span>}
|
||||
{label.type === "enum" && <span>类型:枚举,每行一个取值(示例:一行写一个值)</span>}
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
|
||||
{!hideFooter && (
|
||||
<div style={{ display: "flex", gap: 8 }}>
|
||||
<Button icon={<PlusOutlined />} onClick={addLabel}>
|
||||
添加标签
|
||||
</Button>
|
||||
<Button type="primary" onClick={generate}>
|
||||
生成 JSON 配置
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
);
|
||||
import { Button, Card, Input, InputNumber, Popconfirm, Select, Switch, Tooltip } from "antd";
|
||||
import { PlusOutlined, DeleteOutlined } from "@ant-design/icons";
|
||||
import { useState, useImperativeHandle, forwardRef } from "react";
|
||||
|
||||
type LabelType = "string" | "number" | "enum";
|
||||
|
||||
type LabelItem = {
|
||||
id: string;
|
||||
name: string;
|
||||
type: LabelType;
|
||||
required?: boolean;
|
||||
// for enum: values; for number: min/max
|
||||
values?: string[];
|
||||
min?: number | null;
|
||||
max?: number | null;
|
||||
step?: number | null;
|
||||
};
|
||||
|
||||
type LabelingConfigEditorProps = {
|
||||
initial?: any;
|
||||
onGenerate: (config: any) => void;
|
||||
hideFooter?: boolean;
|
||||
};
|
||||
|
||||
export default forwardRef<any, LabelingConfigEditorProps>(function LabelingConfigEditor(
|
||||
{ initial, onGenerate, hideFooter }: LabelingConfigEditorProps,
|
||||
ref: any
|
||||
) {
|
||||
const [labels, setLabels] = useState<LabelItem[]>(() => {
|
||||
if (initial && initial.labels && Array.isArray(initial.labels)) {
|
||||
return initial.labels.map((l: any, idx: number) => ({
|
||||
id: `${Date.now()}-${idx}`,
|
||||
name: l.name || "",
|
||||
type: l.type || "string",
|
||||
required: !!l.required,
|
||||
values: l.values || (l.type === "enum" ? [] : undefined),
|
||||
min: l.min ?? null,
|
||||
max: l.max ?? null,
|
||||
}));
|
||||
}
|
||||
return [];
|
||||
});
|
||||
|
||||
const addLabel = () => {
|
||||
setLabels((s) => [
|
||||
...s,
|
||||
{ id: `${Date.now()}-${Math.random()}`, name: "", type: "string", required: false, step: null },
|
||||
]);
|
||||
};
|
||||
|
||||
const updateLabel = (id: string, patch: Partial<LabelItem>) => {
|
||||
setLabels((s) => s.map((l) => (l.id === id ? { ...l, ...patch } : l)));
|
||||
};
|
||||
|
||||
const removeLabel = (id: string) => {
|
||||
setLabels((s) => s.filter((l) => l.id !== id));
|
||||
};
|
||||
|
||||
const generate = () => {
|
||||
// basic validation: label name non-empty
|
||||
for (const l of labels) {
|
||||
if (!l.name || l.name.trim() === "") {
|
||||
// focus not available here, just abort
|
||||
// Could show a more friendly UI; keep simple for now
|
||||
// eslint-disable-next-line no-alert
|
||||
alert("请为所有标签填写名称");
|
||||
return;
|
||||
}
|
||||
if (l.type === "enum") {
|
||||
if (!l.values || l.values.length === 0) {
|
||||
alert(`枚举标签 ${l.name} 需要至少一个取值`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (l.type === "number") {
|
||||
// validate min/max
|
||||
if (l.min != null && l.max != null && l.min > l.max) {
|
||||
alert(`数值标签 ${l.name} 的最小值不能大于最大值`);
|
||||
return;
|
||||
}
|
||||
// validate step
|
||||
if (l.step != null && (!(typeof l.step === "number") || l.step <= 0)) {
|
||||
alert(`数值标签 ${l.name} 的步长必须为大于 0 的数值`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const config = {
|
||||
labels: labels.map((l) => {
|
||||
const item: any = { name: l.name, type: l.type, required: !!l.required };
|
||||
if (l.type === "enum") item.values = l.values || [];
|
||||
if (l.type === "number") {
|
||||
if (l.min != null) item.min = l.min;
|
||||
if (l.max != null) item.max = l.max;
|
||||
}
|
||||
return item;
|
||||
}),
|
||||
};
|
||||
|
||||
onGenerate(config);
|
||||
};
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
addLabel,
|
||||
generate,
|
||||
}));
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
|
||||
{labels.map((label) => (
|
||||
<Card size="small" key={label.id} styles={{ body: { padding: 10 } }}>
|
||||
<div style={{ display: "flex", flexWrap: "wrap", gap: 8, alignItems: "center" }}>
|
||||
<Input
|
||||
placeholder="标签名称"
|
||||
value={label.name}
|
||||
onChange={(e) => updateLabel(label.id, { name: e.target.value })}
|
||||
style={{ flex: "1 1 160px", minWidth: 120 }}
|
||||
/>
|
||||
<Select
|
||||
value={label.type}
|
||||
onChange={(v) => updateLabel(label.id, { type: v as LabelType })}
|
||||
options={[{ label: "文本", value: "string" }, { label: "数值", value: "number" }, { label: "枚举", value: "enum" }]}
|
||||
style={{ width: 120, flex: "0 0 120px" }}
|
||||
/>
|
||||
|
||||
{label.type === "enum" && (
|
||||
<Input.TextArea
|
||||
placeholder="每行一个枚举值,按回车换行"
|
||||
value={(label.values || []).join("\n")}
|
||||
onChange={(e) => updateLabel(label.id, { values: e.target.value.split(/\r?\n/).map((s) => s.trim()).filter(Boolean) })}
|
||||
onKeyDown={(e) => {
|
||||
// Prevent parent handlers (like Form submit or modal shortcuts) from intercepting Enter
|
||||
e.stopPropagation();
|
||||
}}
|
||||
rows={3}
|
||||
style={{ flex: "1 1 220px", minWidth: 160, width: "100%", resize: "vertical" }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{label.type === "number" && (
|
||||
<div style={{ display: "flex", gap: 8, alignItems: "center", flex: "0 0 auto" }}>
|
||||
<Tooltip title="最小值">
|
||||
<InputNumber value={label.min ?? null} onChange={(v) => updateLabel(label.id, { min: v ?? null })} placeholder="min" />
|
||||
</Tooltip>
|
||||
<Tooltip title="最大值">
|
||||
<InputNumber value={label.max ?? null} onChange={(v) => updateLabel(label.id, { max: v ?? null })} placeholder="max" />
|
||||
</Tooltip>
|
||||
<Tooltip title="步长 (step)">
|
||||
<InputNumber value={label.step ?? null} onChange={(v) => updateLabel(label.id, { step: v ?? null })} placeholder="step" min={0} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 8, marginLeft: "auto" }}>
|
||||
<span style={{ fontSize: 12, color: "rgba(0,0,0,0.65)" }}>必填</span>
|
||||
<Switch checked={!!label.required} onChange={(v) => updateLabel(label.id, { required: v })} />
|
||||
<Popconfirm title="确认删除该标签?" onConfirm={() => removeLabel(label.id)}>
|
||||
<Button type="text" icon={<DeleteOutlined />} />
|
||||
</Popconfirm>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ marginTop: 8, color: "rgba(0,0,0,0.45)", fontSize: 12 }}>
|
||||
{label.type === "string" && <span>类型:文本</span>}
|
||||
{label.type === "number" && <span>类型:数值,支持 min / max / step</span>}
|
||||
{label.type === "enum" && <span>类型:枚举,每行一个取值(示例:一行写一个值)</span>}
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
|
||||
{!hideFooter && (
|
||||
<div style={{ display: "flex", gap: 8 }}>
|
||||
<Button icon={<PlusOutlined />} onClick={addLabel}>
|
||||
添加标签
|
||||
</Button>
|
||||
<Button type="primary" onClick={generate}>
|
||||
生成 JSON 配置
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
);
|
||||
|
||||
@@ -1,398 +1,398 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { Card, Button, Table, message, Modal, Tabs } from "antd";
|
||||
import {
|
||||
PlusOutlined,
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
SyncOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { SearchControls } from "@/components/SearchControls";
|
||||
import CardView from "@/components/CardView";
|
||||
import type { AnnotationTask } from "../annotation.model";
|
||||
import useFetchData from "@/hooks/useFetchData";
|
||||
import {
|
||||
deleteAnnotationTaskByIdUsingDelete, loginAnnotationUsingGet,
|
||||
queryAnnotationTasksUsingGet,
|
||||
syncAnnotationTaskUsingPost,
|
||||
} from "../annotation.api";
|
||||
import { mapAnnotationTask } from "../annotation.const";
|
||||
import CreateAnnotationTask from "../Create/components/CreateAnnotationTaskDialog";
|
||||
import { ColumnType } from "antd/es/table";
|
||||
import { TemplateList } from "../Template";
|
||||
// Note: DevelopmentInProgress intentionally not used here
|
||||
|
||||
export default function DataAnnotation() {
|
||||
// return <DevelopmentInProgress showTime="2025.10.30" />;
|
||||
const [activeTab, setActiveTab] = useState("tasks");
|
||||
const [viewMode, setViewMode] = useState<"list" | "card">("list");
|
||||
const [showCreateDialog, setShowCreateDialog] = useState(false);
|
||||
|
||||
const {
|
||||
loading,
|
||||
tableData,
|
||||
pagination,
|
||||
searchParams,
|
||||
fetchData,
|
||||
handleFiltersChange,
|
||||
handleKeywordChange,
|
||||
} = useFetchData(queryAnnotationTasksUsingGet, mapAnnotationTask, 30000, true, [], 0);
|
||||
|
||||
const [labelStudioBase, setLabelStudioBase] = useState<string | null>(null);
|
||||
const [selectedRowKeys, setSelectedRowKeys] = useState<(string | number)[]>([]);
|
||||
const [selectedRows, setSelectedRows] = useState<any[]>([]);
|
||||
|
||||
// prefetch config on mount so clicking annotate is fast and we know whether base URL exists
|
||||
// useEffect ensures this runs once
|
||||
useEffect(() => {
|
||||
let mounted = true;
|
||||
(async () => {
|
||||
try {
|
||||
const baseUrl = `http://${window.location.hostname}:${parseInt(window.location.port) + 1}`;
|
||||
if (mounted) setLabelStudioBase(baseUrl);
|
||||
} catch (e) {
|
||||
if (mounted) setLabelStudioBase(null);
|
||||
}
|
||||
})();
|
||||
return () => {
|
||||
mounted = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleAnnotate = (task: AnnotationTask) => {
|
||||
// Open Label Studio project page in a new tab
|
||||
(async () => {
|
||||
try {
|
||||
// prefer using labeling project id already present on the task
|
||||
// `mapAnnotationTask` normalizes upstream fields into `labelingProjId`/`projId`,
|
||||
// so prefer those and fall back to the task id if necessary.
|
||||
let labelingProjId = (task as any).labelingProjId || (task as any).projId || undefined;
|
||||
|
||||
// no fallback external mapping lookup; rely on normalized fields from mapAnnotationTask
|
||||
|
||||
// use prefetched base if available
|
||||
const base = labelStudioBase;
|
||||
|
||||
// no debug logging in production
|
||||
|
||||
if (labelingProjId) {
|
||||
// only open external Label Studio when we have a configured base url
|
||||
await loginAnnotationUsingGet(labelingProjId)
|
||||
if (base) {
|
||||
const target = `${base}/projects/${labelingProjId}/data`;
|
||||
window.open(target, "_blank");
|
||||
} else {
|
||||
// no external Label Studio URL configured — do not perform internal redirect in this version
|
||||
message.error("无法跳转到 Label Studio:未配置 Label Studio 基础 URL");
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// no labeling project id available — do not attempt internal redirect in this version
|
||||
message.error("无法跳转到 Label Studio:该映射未绑定标注项目");
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
// on error, surface a user-friendly message instead of redirecting
|
||||
message.error("无法跳转到 Label Studio:发生错误,请检查配置或控制台日志");
|
||||
return;
|
||||
}
|
||||
})();
|
||||
};
|
||||
|
||||
const handleDelete = (task: AnnotationTask) => {
|
||||
Modal.confirm({
|
||||
title: `确认删除标注任务「${task.name}」吗?`,
|
||||
content: (
|
||||
<div>
|
||||
<div>删除标注任务不会删除对应数据集。</div>
|
||||
<div>如需保留当前标注结果,请在同步后再删除。</div>
|
||||
</div>
|
||||
),
|
||||
okText: "删除",
|
||||
okType: "danger",
|
||||
cancelText: "取消",
|
||||
onOk: async () => {
|
||||
try {
|
||||
await deleteAnnotationTaskByIdUsingDelete(task.id);
|
||||
message.success("映射删除成功");
|
||||
fetchData();
|
||||
// clear selection if deleted item was selected
|
||||
setSelectedRowKeys((keys) => keys.filter((k) => k !== task.id));
|
||||
setSelectedRows((rows) => rows.filter((r) => r.id !== task.id));
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
message.error("删除失败,请稍后重试");
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleSync = (task: AnnotationTask, batchSize: number = 50) => {
|
||||
Modal.confirm({
|
||||
title: `确认同步标注任务「${task.name}」吗?`,
|
||||
content: (
|
||||
<div>
|
||||
<div>标注工程中文件列表将与数据集保持一致,差异项将会被修正。</div>
|
||||
<div>标注工程中的标签与数据集中标签将进行合并,冲突项将以最新一次内容为准。</div>
|
||||
</div>
|
||||
),
|
||||
okText: "同步",
|
||||
cancelText: "取消",
|
||||
onOk: async () => {
|
||||
try {
|
||||
await syncAnnotationTaskUsingPost({ id: task.id, batchSize });
|
||||
message.success("任务同步请求已发送");
|
||||
// optional: refresh list/status
|
||||
fetchData();
|
||||
// clear selection for the task
|
||||
setSelectedRowKeys((keys) => keys.filter((k) => k !== task.id));
|
||||
setSelectedRows((rows) => rows.filter((r) => r.id !== task.id));
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
message.error("同步失败,请稍后重试");
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleBatchSync = (batchSize: number = 50) => {
|
||||
if (!selectedRows || selectedRows.length === 0) return;
|
||||
Modal.confirm({
|
||||
title: `确认同步所选 ${selectedRows.length} 个标注任务吗?`,
|
||||
content: (
|
||||
<div>
|
||||
<div>标注工程中文件列表将与数据集保持一致,差异项将会被修正。</div>
|
||||
<div>标注工程中的标签与数据集中标签将进行合并,冲突项将以最新一次内容为准。</div>
|
||||
</div>
|
||||
),
|
||||
okText: "同步",
|
||||
cancelText: "取消",
|
||||
onOk: async () => {
|
||||
try {
|
||||
await Promise.all(
|
||||
selectedRows.map((r) => syncAnnotationTaskUsingPost({ id: r.id, batchSize }))
|
||||
);
|
||||
message.success("批量同步请求已发送");
|
||||
fetchData();
|
||||
setSelectedRowKeys([]);
|
||||
setSelectedRows([]);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
message.error("批量同步失败,请稍后重试");
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleBatchDelete = () => {
|
||||
if (!selectedRows || selectedRows.length === 0) return;
|
||||
Modal.confirm({
|
||||
title: `确认删除所选 ${selectedRows.length} 个标注任务吗?`,
|
||||
content: (
|
||||
<div>
|
||||
<div>删除标注任务不会删除对应数据集。</div>
|
||||
<div>如需保留当前标注结果,请在同步后再删除。</div>
|
||||
</div>
|
||||
),
|
||||
okText: "删除",
|
||||
okType: "danger",
|
||||
cancelText: "取消",
|
||||
onOk: async () => {
|
||||
try {
|
||||
await Promise.all(
|
||||
selectedRows.map((r) => deleteAnnotationTaskByIdUsingDelete(r.id))
|
||||
);
|
||||
message.success("批量删除已完成");
|
||||
fetchData();
|
||||
setSelectedRowKeys([]);
|
||||
setSelectedRows([]);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
message.error("批量删除失败,请稍后重试");
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const operations = [
|
||||
{
|
||||
key: "annotate",
|
||||
label: "标注",
|
||||
icon: (
|
||||
<EditOutlined
|
||||
className="w-4 h-4 text-green-400"
|
||||
style={{ color: "#52c41a" }}
|
||||
/>
|
||||
),
|
||||
onClick: handleAnnotate,
|
||||
},
|
||||
{
|
||||
key: "sync",
|
||||
label: "同步",
|
||||
icon: <SyncOutlined className="w-4 h-4" style={{ color: "#722ed1" }} />,
|
||||
onClick: handleSync,
|
||||
},
|
||||
{
|
||||
key: "delete",
|
||||
label: "删除",
|
||||
icon: <DeleteOutlined style={{ color: "#f5222d" }} />,
|
||||
onClick: handleDelete,
|
||||
},
|
||||
];
|
||||
|
||||
const columns: ColumnType<any>[] = [
|
||||
{
|
||||
title: "任务名称",
|
||||
dataIndex: "name",
|
||||
key: "name",
|
||||
fixed: "left" as const,
|
||||
},
|
||||
{
|
||||
title: "任务ID",
|
||||
dataIndex: "id",
|
||||
key: "id",
|
||||
},
|
||||
{
|
||||
title: "数据集",
|
||||
dataIndex: "datasetName",
|
||||
key: "datasetName",
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
title: "创建时间",
|
||||
dataIndex: "createdAt",
|
||||
key: "createdAt",
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
title: "更新时间",
|
||||
dataIndex: "updatedAt",
|
||||
key: "updatedAt",
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
key: "actions",
|
||||
fixed: "right" as const,
|
||||
width: 150,
|
||||
dataIndex: "actions",
|
||||
render: (_: any, task: any) => (
|
||||
<div className="flex items-center justify-center space-x-1">
|
||||
{operations.map((operation) => (
|
||||
<Button
|
||||
key={operation.key}
|
||||
type="text"
|
||||
icon={operation.icon}
|
||||
onClick={() => (operation?.onClick as any)?.(task)}
|
||||
title={operation.label}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full gap-4">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="text-xl font-bold">数据标注</h1>
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<Tabs
|
||||
activeKey={activeTab}
|
||||
onChange={setActiveTab}
|
||||
items={[
|
||||
{
|
||||
key: "tasks",
|
||||
label: "标注任务",
|
||||
children: (
|
||||
<div className="flex flex-col gap-4">
|
||||
{/* Search, Filters and Buttons in one row */}
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
{/* Left side: Search and view controls */}
|
||||
<div className="flex items-center gap-2">
|
||||
<SearchControls
|
||||
searchTerm={searchParams.keyword}
|
||||
onSearchChange={handleKeywordChange}
|
||||
searchPlaceholder="搜索任务名称、描述"
|
||||
onFiltersChange={handleFiltersChange}
|
||||
viewMode={viewMode}
|
||||
onViewModeChange={setViewMode}
|
||||
showViewToggle={true}
|
||||
onReload={fetchData}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Right side: All action buttons */}
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
onClick={() => handleBatchSync(50)}
|
||||
disabled={selectedRowKeys.length === 0}
|
||||
>
|
||||
批量同步
|
||||
</Button>
|
||||
<Button
|
||||
danger
|
||||
onClick={handleBatchDelete}
|
||||
disabled={selectedRowKeys.length === 0}
|
||||
>
|
||||
批量删除
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => setShowCreateDialog(true)}
|
||||
>
|
||||
创建标注任务
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Task List/Card */}
|
||||
{viewMode === "list" ? (
|
||||
<Card>
|
||||
<Table
|
||||
key="id"
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
columns={columns}
|
||||
dataSource={tableData}
|
||||
pagination={pagination}
|
||||
rowSelection={{
|
||||
selectedRowKeys,
|
||||
onChange: (keys, rows) => {
|
||||
setSelectedRowKeys(keys as (string | number)[]);
|
||||
setSelectedRows(rows as any[]);
|
||||
},
|
||||
}}
|
||||
scroll={{ x: "max-content", y: "calc(100vh - 24rem)" }}
|
||||
/>
|
||||
</Card>
|
||||
) : (
|
||||
<CardView
|
||||
data={tableData}
|
||||
operations={operations as any}
|
||||
pagination={pagination}
|
||||
loading={loading}
|
||||
/>
|
||||
)}
|
||||
|
||||
<CreateAnnotationTask
|
||||
open={showCreateDialog}
|
||||
onClose={() => setShowCreateDialog(false)}
|
||||
onRefresh={fetchData}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "templates",
|
||||
label: "标注模板",
|
||||
children: <TemplateList />,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
import { useState, useEffect } from "react";
|
||||
import { Card, Button, Table, message, Modal, Tabs } from "antd";
|
||||
import {
|
||||
PlusOutlined,
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
SyncOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { SearchControls } from "@/components/SearchControls";
|
||||
import CardView from "@/components/CardView";
|
||||
import type { AnnotationTask } from "../annotation.model";
|
||||
import useFetchData from "@/hooks/useFetchData";
|
||||
import {
|
||||
deleteAnnotationTaskByIdUsingDelete, loginAnnotationUsingGet,
|
||||
queryAnnotationTasksUsingGet,
|
||||
syncAnnotationTaskUsingPost,
|
||||
} from "../annotation.api";
|
||||
import { mapAnnotationTask } from "../annotation.const";
|
||||
import CreateAnnotationTask from "../Create/components/CreateAnnotationTaskDialog";
|
||||
import { ColumnType } from "antd/es/table";
|
||||
import { TemplateList } from "../Template";
|
||||
// Note: DevelopmentInProgress intentionally not used here
|
||||
|
||||
export default function DataAnnotation() {
|
||||
// return <DevelopmentInProgress showTime="2025.10.30" />;
|
||||
const [activeTab, setActiveTab] = useState("tasks");
|
||||
const [viewMode, setViewMode] = useState<"list" | "card">("list");
|
||||
const [showCreateDialog, setShowCreateDialog] = useState(false);
|
||||
|
||||
const {
|
||||
loading,
|
||||
tableData,
|
||||
pagination,
|
||||
searchParams,
|
||||
fetchData,
|
||||
handleFiltersChange,
|
||||
handleKeywordChange,
|
||||
} = useFetchData(queryAnnotationTasksUsingGet, mapAnnotationTask, 30000, true, [], 0);
|
||||
|
||||
const [labelStudioBase, setLabelStudioBase] = useState<string | null>(null);
|
||||
const [selectedRowKeys, setSelectedRowKeys] = useState<(string | number)[]>([]);
|
||||
const [selectedRows, setSelectedRows] = useState<any[]>([]);
|
||||
|
||||
// prefetch config on mount so clicking annotate is fast and we know whether base URL exists
|
||||
// useEffect ensures this runs once
|
||||
useEffect(() => {
|
||||
let mounted = true;
|
||||
(async () => {
|
||||
try {
|
||||
const baseUrl = `http://${window.location.hostname}:${parseInt(window.location.port) + 1}`;
|
||||
if (mounted) setLabelStudioBase(baseUrl);
|
||||
} catch (e) {
|
||||
if (mounted) setLabelStudioBase(null);
|
||||
}
|
||||
})();
|
||||
return () => {
|
||||
mounted = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleAnnotate = (task: AnnotationTask) => {
|
||||
// Open Label Studio project page in a new tab
|
||||
(async () => {
|
||||
try {
|
||||
// prefer using labeling project id already present on the task
|
||||
// `mapAnnotationTask` normalizes upstream fields into `labelingProjId`/`projId`,
|
||||
// so prefer those and fall back to the task id if necessary.
|
||||
let labelingProjId = (task as any).labelingProjId || (task as any).projId || undefined;
|
||||
|
||||
// no fallback external mapping lookup; rely on normalized fields from mapAnnotationTask
|
||||
|
||||
// use prefetched base if available
|
||||
const base = labelStudioBase;
|
||||
|
||||
// no debug logging in production
|
||||
|
||||
if (labelingProjId) {
|
||||
// only open external Label Studio when we have a configured base url
|
||||
await loginAnnotationUsingGet(labelingProjId)
|
||||
if (base) {
|
||||
const target = `${base}/projects/${labelingProjId}/data`;
|
||||
window.open(target, "_blank");
|
||||
} else {
|
||||
// no external Label Studio URL configured — do not perform internal redirect in this version
|
||||
message.error("无法跳转到 Label Studio:未配置 Label Studio 基础 URL");
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// no labeling project id available — do not attempt internal redirect in this version
|
||||
message.error("无法跳转到 Label Studio:该映射未绑定标注项目");
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
// on error, surface a user-friendly message instead of redirecting
|
||||
message.error("无法跳转到 Label Studio:发生错误,请检查配置或控制台日志");
|
||||
return;
|
||||
}
|
||||
})();
|
||||
};
|
||||
|
||||
const handleDelete = (task: AnnotationTask) => {
|
||||
Modal.confirm({
|
||||
title: `确认删除标注任务「${task.name}」吗?`,
|
||||
content: (
|
||||
<div>
|
||||
<div>删除标注任务不会删除对应数据集。</div>
|
||||
<div>如需保留当前标注结果,请在同步后再删除。</div>
|
||||
</div>
|
||||
),
|
||||
okText: "删除",
|
||||
okType: "danger",
|
||||
cancelText: "取消",
|
||||
onOk: async () => {
|
||||
try {
|
||||
await deleteAnnotationTaskByIdUsingDelete(task.id);
|
||||
message.success("映射删除成功");
|
||||
fetchData();
|
||||
// clear selection if deleted item was selected
|
||||
setSelectedRowKeys((keys) => keys.filter((k) => k !== task.id));
|
||||
setSelectedRows((rows) => rows.filter((r) => r.id !== task.id));
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
message.error("删除失败,请稍后重试");
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleSync = (task: AnnotationTask, batchSize: number = 50) => {
|
||||
Modal.confirm({
|
||||
title: `确认同步标注任务「${task.name}」吗?`,
|
||||
content: (
|
||||
<div>
|
||||
<div>标注工程中文件列表将与数据集保持一致,差异项将会被修正。</div>
|
||||
<div>标注工程中的标签与数据集中标签将进行合并,冲突项将以最新一次内容为准。</div>
|
||||
</div>
|
||||
),
|
||||
okText: "同步",
|
||||
cancelText: "取消",
|
||||
onOk: async () => {
|
||||
try {
|
||||
await syncAnnotationTaskUsingPost({ id: task.id, batchSize });
|
||||
message.success("任务同步请求已发送");
|
||||
// optional: refresh list/status
|
||||
fetchData();
|
||||
// clear selection for the task
|
||||
setSelectedRowKeys((keys) => keys.filter((k) => k !== task.id));
|
||||
setSelectedRows((rows) => rows.filter((r) => r.id !== task.id));
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
message.error("同步失败,请稍后重试");
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleBatchSync = (batchSize: number = 50) => {
|
||||
if (!selectedRows || selectedRows.length === 0) return;
|
||||
Modal.confirm({
|
||||
title: `确认同步所选 ${selectedRows.length} 个标注任务吗?`,
|
||||
content: (
|
||||
<div>
|
||||
<div>标注工程中文件列表将与数据集保持一致,差异项将会被修正。</div>
|
||||
<div>标注工程中的标签与数据集中标签将进行合并,冲突项将以最新一次内容为准。</div>
|
||||
</div>
|
||||
),
|
||||
okText: "同步",
|
||||
cancelText: "取消",
|
||||
onOk: async () => {
|
||||
try {
|
||||
await Promise.all(
|
||||
selectedRows.map((r) => syncAnnotationTaskUsingPost({ id: r.id, batchSize }))
|
||||
);
|
||||
message.success("批量同步请求已发送");
|
||||
fetchData();
|
||||
setSelectedRowKeys([]);
|
||||
setSelectedRows([]);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
message.error("批量同步失败,请稍后重试");
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleBatchDelete = () => {
|
||||
if (!selectedRows || selectedRows.length === 0) return;
|
||||
Modal.confirm({
|
||||
title: `确认删除所选 ${selectedRows.length} 个标注任务吗?`,
|
||||
content: (
|
||||
<div>
|
||||
<div>删除标注任务不会删除对应数据集。</div>
|
||||
<div>如需保留当前标注结果,请在同步后再删除。</div>
|
||||
</div>
|
||||
),
|
||||
okText: "删除",
|
||||
okType: "danger",
|
||||
cancelText: "取消",
|
||||
onOk: async () => {
|
||||
try {
|
||||
await Promise.all(
|
||||
selectedRows.map((r) => deleteAnnotationTaskByIdUsingDelete(r.id))
|
||||
);
|
||||
message.success("批量删除已完成");
|
||||
fetchData();
|
||||
setSelectedRowKeys([]);
|
||||
setSelectedRows([]);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
message.error("批量删除失败,请稍后重试");
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const operations = [
|
||||
{
|
||||
key: "annotate",
|
||||
label: "标注",
|
||||
icon: (
|
||||
<EditOutlined
|
||||
className="w-4 h-4 text-green-400"
|
||||
style={{ color: "#52c41a" }}
|
||||
/>
|
||||
),
|
||||
onClick: handleAnnotate,
|
||||
},
|
||||
{
|
||||
key: "sync",
|
||||
label: "同步",
|
||||
icon: <SyncOutlined className="w-4 h-4" style={{ color: "#722ed1" }} />,
|
||||
onClick: handleSync,
|
||||
},
|
||||
{
|
||||
key: "delete",
|
||||
label: "删除",
|
||||
icon: <DeleteOutlined style={{ color: "#f5222d" }} />,
|
||||
onClick: handleDelete,
|
||||
},
|
||||
];
|
||||
|
||||
const columns: ColumnType<any>[] = [
|
||||
{
|
||||
title: "任务名称",
|
||||
dataIndex: "name",
|
||||
key: "name",
|
||||
fixed: "left" as const,
|
||||
},
|
||||
{
|
||||
title: "任务ID",
|
||||
dataIndex: "id",
|
||||
key: "id",
|
||||
},
|
||||
{
|
||||
title: "数据集",
|
||||
dataIndex: "datasetName",
|
||||
key: "datasetName",
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
title: "创建时间",
|
||||
dataIndex: "createdAt",
|
||||
key: "createdAt",
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
title: "更新时间",
|
||||
dataIndex: "updatedAt",
|
||||
key: "updatedAt",
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
key: "actions",
|
||||
fixed: "right" as const,
|
||||
width: 150,
|
||||
dataIndex: "actions",
|
||||
render: (_: any, task: any) => (
|
||||
<div className="flex items-center justify-center space-x-1">
|
||||
{operations.map((operation) => (
|
||||
<Button
|
||||
key={operation.key}
|
||||
type="text"
|
||||
icon={operation.icon}
|
||||
onClick={() => (operation?.onClick as any)?.(task)}
|
||||
title={operation.label}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full gap-4">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="text-xl font-bold">数据标注</h1>
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<Tabs
|
||||
activeKey={activeTab}
|
||||
onChange={setActiveTab}
|
||||
items={[
|
||||
{
|
||||
key: "tasks",
|
||||
label: "标注任务",
|
||||
children: (
|
||||
<div className="flex flex-col gap-4">
|
||||
{/* Search, Filters and Buttons in one row */}
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
{/* Left side: Search and view controls */}
|
||||
<div className="flex items-center gap-2">
|
||||
<SearchControls
|
||||
searchTerm={searchParams.keyword}
|
||||
onSearchChange={handleKeywordChange}
|
||||
searchPlaceholder="搜索任务名称、描述"
|
||||
onFiltersChange={handleFiltersChange}
|
||||
viewMode={viewMode}
|
||||
onViewModeChange={setViewMode}
|
||||
showViewToggle={true}
|
||||
onReload={fetchData}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Right side: All action buttons */}
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
onClick={() => handleBatchSync(50)}
|
||||
disabled={selectedRowKeys.length === 0}
|
||||
>
|
||||
批量同步
|
||||
</Button>
|
||||
<Button
|
||||
danger
|
||||
onClick={handleBatchDelete}
|
||||
disabled={selectedRowKeys.length === 0}
|
||||
>
|
||||
批量删除
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => setShowCreateDialog(true)}
|
||||
>
|
||||
创建标注任务
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Task List/Card */}
|
||||
{viewMode === "list" ? (
|
||||
<Card>
|
||||
<Table
|
||||
key="id"
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
columns={columns}
|
||||
dataSource={tableData}
|
||||
pagination={pagination}
|
||||
rowSelection={{
|
||||
selectedRowKeys,
|
||||
onChange: (keys, rows) => {
|
||||
setSelectedRowKeys(keys as (string | number)[]);
|
||||
setSelectedRows(rows as any[]);
|
||||
},
|
||||
}}
|
||||
scroll={{ x: "max-content", y: "calc(100vh - 24rem)" }}
|
||||
/>
|
||||
</Card>
|
||||
) : (
|
||||
<CardView
|
||||
data={tableData}
|
||||
operations={operations as any}
|
||||
pagination={pagination}
|
||||
loading={loading}
|
||||
/>
|
||||
)}
|
||||
|
||||
<CreateAnnotationTask
|
||||
open={showCreateDialog}
|
||||
onClose={() => setShowCreateDialog(false)}
|
||||
onRefresh={fetchData}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "templates",
|
||||
label: "标注模板",
|
||||
children: <TemplateList />,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,155 +1,155 @@
|
||||
import React from "react";
|
||||
import { Modal, Descriptions, Tag, Space, Divider, Card, Typography } from "antd";
|
||||
import type { AnnotationTemplate } from "../annotation.model";
|
||||
|
||||
const { Text, Paragraph } = Typography;
|
||||
|
||||
interface TemplateDetailProps {
|
||||
visible: boolean;
|
||||
template?: AnnotationTemplate;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const TemplateDetail: React.FC<TemplateDetailProps> = ({
|
||||
visible,
|
||||
template,
|
||||
onClose,
|
||||
}) => {
|
||||
if (!template) return null;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="模板详情"
|
||||
open={visible}
|
||||
onCancel={onClose}
|
||||
footer={null}
|
||||
width={800}
|
||||
>
|
||||
<Descriptions bordered column={2}>
|
||||
<Descriptions.Item label="名称" span={2}>
|
||||
{template.name}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="描述" span={2}>
|
||||
{template.description || "-"}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="数据类型">
|
||||
<Tag color="cyan">{template.dataType}</Tag>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="标注类型">
|
||||
<Tag color="geekblue">{template.labelingType}</Tag>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="分类">
|
||||
<Tag color="blue">{template.category}</Tag>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="样式">
|
||||
{template.style}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="类型">
|
||||
<Tag color={template.builtIn ? "gold" : "default"}>
|
||||
{template.builtIn ? "系统内置" : "自定义"}
|
||||
</Tag>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="版本">
|
||||
{template.version}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="创建时间" span={2}>
|
||||
{new Date(template.createdAt).toLocaleString()}
|
||||
</Descriptions.Item>
|
||||
{template.updatedAt && (
|
||||
<Descriptions.Item label="更新时间" span={2}>
|
||||
{new Date(template.updatedAt).toLocaleString()}
|
||||
</Descriptions.Item>
|
||||
)}
|
||||
</Descriptions>
|
||||
|
||||
<Divider>配置详情</Divider>
|
||||
|
||||
<Card title="数据对象" size="small" style={{ marginBottom: 16 }}>
|
||||
<Space direction="vertical" style={{ width: "100%" }}>
|
||||
{template.configuration.objects.map((obj, index) => (
|
||||
<Card key={index} size="small" type="inner">
|
||||
<Space>
|
||||
<Text strong>名称:</Text>
|
||||
<Tag>{obj.name}</Tag>
|
||||
<Text strong>类型:</Text>
|
||||
<Tag color="blue">{obj.type}</Tag>
|
||||
<Text strong>值:</Text>
|
||||
<Tag color="green">{obj.value}</Tag>
|
||||
</Space>
|
||||
</Card>
|
||||
))}
|
||||
</Space>
|
||||
</Card>
|
||||
|
||||
<Card title="标注控件" size="small" style={{ marginBottom: 16 }}>
|
||||
<Space direction="vertical" style={{ width: "100%" }} size="middle">
|
||||
{template.configuration.labels.map((label, index) => (
|
||||
<Card key={index} size="small" type="inner" title={`控件 ${index + 1}`}>
|
||||
<Space direction="vertical" style={{ width: "100%" }}>
|
||||
<div>
|
||||
<Text strong>来源名称:</Text>
|
||||
<Tag>{label.fromName}</Tag>
|
||||
|
||||
<Text strong style={{ marginLeft: 16 }}>目标名称:</Text>
|
||||
<Tag>{label.toName}</Tag>
|
||||
|
||||
<Text strong style={{ marginLeft: 16 }}>类型:</Text>
|
||||
<Tag color="purple">{label.type}</Tag>
|
||||
|
||||
{label.required && <Tag color="red">必填</Tag>}
|
||||
</div>
|
||||
|
||||
{label.description && (
|
||||
<div>
|
||||
<Text strong>描述:</Text>
|
||||
<Text type="secondary">{label.description}</Text>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{label.options && label.options.length > 0 && (
|
||||
<div>
|
||||
<Text strong>选项:</Text>
|
||||
<div style={{ marginTop: 4 }}>
|
||||
{label.options.map((opt, i) => (
|
||||
<Tag key={i} color="cyan">{opt}</Tag>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{label.labels && label.labels.length > 0 && (
|
||||
<div>
|
||||
<Text strong>标签:</Text>
|
||||
<div style={{ marginTop: 4 }}>
|
||||
{label.labels.map((lbl, i) => (
|
||||
<Tag key={i} color="geekblue">{lbl}</Tag>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Space>
|
||||
</Card>
|
||||
))}
|
||||
</Space>
|
||||
</Card>
|
||||
|
||||
{template.labelConfig && (
|
||||
<Card title="Label Studio XML 配置" size="small">
|
||||
<Paragraph>
|
||||
<pre style={{
|
||||
background: "#f5f5f5",
|
||||
padding: 12,
|
||||
borderRadius: 4,
|
||||
overflow: "auto",
|
||||
maxHeight: 300
|
||||
}}>
|
||||
{template.labelConfig}
|
||||
</pre>
|
||||
</Paragraph>
|
||||
</Card>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default TemplateDetail;
|
||||
import React from "react";
|
||||
import { Modal, Descriptions, Tag, Space, Divider, Card, Typography } from "antd";
|
||||
import type { AnnotationTemplate } from "../annotation.model";
|
||||
|
||||
const { Text, Paragraph } = Typography;
|
||||
|
||||
interface TemplateDetailProps {
|
||||
visible: boolean;
|
||||
template?: AnnotationTemplate;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const TemplateDetail: React.FC<TemplateDetailProps> = ({
|
||||
visible,
|
||||
template,
|
||||
onClose,
|
||||
}) => {
|
||||
if (!template) return null;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="模板详情"
|
||||
open={visible}
|
||||
onCancel={onClose}
|
||||
footer={null}
|
||||
width={800}
|
||||
>
|
||||
<Descriptions bordered column={2}>
|
||||
<Descriptions.Item label="名称" span={2}>
|
||||
{template.name}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="描述" span={2}>
|
||||
{template.description || "-"}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="数据类型">
|
||||
<Tag color="cyan">{template.dataType}</Tag>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="标注类型">
|
||||
<Tag color="geekblue">{template.labelingType}</Tag>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="分类">
|
||||
<Tag color="blue">{template.category}</Tag>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="样式">
|
||||
{template.style}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="类型">
|
||||
<Tag color={template.builtIn ? "gold" : "default"}>
|
||||
{template.builtIn ? "系统内置" : "自定义"}
|
||||
</Tag>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="版本">
|
||||
{template.version}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="创建时间" span={2}>
|
||||
{new Date(template.createdAt).toLocaleString()}
|
||||
</Descriptions.Item>
|
||||
{template.updatedAt && (
|
||||
<Descriptions.Item label="更新时间" span={2}>
|
||||
{new Date(template.updatedAt).toLocaleString()}
|
||||
</Descriptions.Item>
|
||||
)}
|
||||
</Descriptions>
|
||||
|
||||
<Divider>配置详情</Divider>
|
||||
|
||||
<Card title="数据对象" size="small" style={{ marginBottom: 16 }}>
|
||||
<Space direction="vertical" style={{ width: "100%" }}>
|
||||
{template.configuration.objects.map((obj, index) => (
|
||||
<Card key={index} size="small" type="inner">
|
||||
<Space>
|
||||
<Text strong>名称:</Text>
|
||||
<Tag>{obj.name}</Tag>
|
||||
<Text strong>类型:</Text>
|
||||
<Tag color="blue">{obj.type}</Tag>
|
||||
<Text strong>值:</Text>
|
||||
<Tag color="green">{obj.value}</Tag>
|
||||
</Space>
|
||||
</Card>
|
||||
))}
|
||||
</Space>
|
||||
</Card>
|
||||
|
||||
<Card title="标注控件" size="small" style={{ marginBottom: 16 }}>
|
||||
<Space direction="vertical" style={{ width: "100%" }} size="middle">
|
||||
{template.configuration.labels.map((label, index) => (
|
||||
<Card key={index} size="small" type="inner" title={`控件 ${index + 1}`}>
|
||||
<Space direction="vertical" style={{ width: "100%" }}>
|
||||
<div>
|
||||
<Text strong>来源名称:</Text>
|
||||
<Tag>{label.fromName}</Tag>
|
||||
|
||||
<Text strong style={{ marginLeft: 16 }}>目标名称:</Text>
|
||||
<Tag>{label.toName}</Tag>
|
||||
|
||||
<Text strong style={{ marginLeft: 16 }}>类型:</Text>
|
||||
<Tag color="purple">{label.type}</Tag>
|
||||
|
||||
{label.required && <Tag color="red">必填</Tag>}
|
||||
</div>
|
||||
|
||||
{label.description && (
|
||||
<div>
|
||||
<Text strong>描述:</Text>
|
||||
<Text type="secondary">{label.description}</Text>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{label.options && label.options.length > 0 && (
|
||||
<div>
|
||||
<Text strong>选项:</Text>
|
||||
<div style={{ marginTop: 4 }}>
|
||||
{label.options.map((opt, i) => (
|
||||
<Tag key={i} color="cyan">{opt}</Tag>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{label.labels && label.labels.length > 0 && (
|
||||
<div>
|
||||
<Text strong>标签:</Text>
|
||||
<div style={{ marginTop: 4 }}>
|
||||
{label.labels.map((lbl, i) => (
|
||||
<Tag key={i} color="geekblue">{lbl}</Tag>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Space>
|
||||
</Card>
|
||||
))}
|
||||
</Space>
|
||||
</Card>
|
||||
|
||||
{template.labelConfig && (
|
||||
<Card title="Label Studio XML 配置" size="small">
|
||||
<Paragraph>
|
||||
<pre style={{
|
||||
background: "#f5f5f5",
|
||||
padding: 12,
|
||||
borderRadius: 4,
|
||||
overflow: "auto",
|
||||
maxHeight: 300
|
||||
}}>
|
||||
{template.labelConfig}
|
||||
</pre>
|
||||
</Paragraph>
|
||||
</Card>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default TemplateDetail;
|
||||
|
||||
@@ -1,427 +1,427 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import {
|
||||
Modal,
|
||||
Form,
|
||||
Input,
|
||||
Select,
|
||||
Button,
|
||||
Space,
|
||||
message,
|
||||
Divider,
|
||||
Card,
|
||||
Checkbox,
|
||||
} from "antd";
|
||||
import { PlusOutlined, MinusCircleOutlined } from "@ant-design/icons";
|
||||
import {
|
||||
createAnnotationTemplateUsingPost,
|
||||
updateAnnotationTemplateByIdUsingPut,
|
||||
} from "../annotation.api";
|
||||
import type { AnnotationTemplate } from "../annotation.model";
|
||||
import TagSelector from "./components/TagSelector";
|
||||
|
||||
const { TextArea } = Input;
|
||||
const { Option } = Select;
|
||||
|
||||
interface TemplateFormProps {
|
||||
visible: boolean;
|
||||
mode: "create" | "edit";
|
||||
template?: AnnotationTemplate;
|
||||
onSuccess: () => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
const TemplateForm: React.FC<TemplateFormProps> = ({
|
||||
visible,
|
||||
mode,
|
||||
template,
|
||||
onSuccess,
|
||||
onCancel,
|
||||
}) => {
|
||||
const [form] = Form.useForm();
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (visible && template && mode === "edit") {
|
||||
form.setFieldsValue({
|
||||
name: template.name,
|
||||
description: template.description,
|
||||
dataType: template.dataType,
|
||||
labelingType: template.labelingType,
|
||||
style: template.style,
|
||||
category: template.category,
|
||||
labels: template.configuration.labels,
|
||||
objects: template.configuration.objects,
|
||||
});
|
||||
} else if (visible && mode === "create") {
|
||||
form.resetFields();
|
||||
// Set default values
|
||||
form.setFieldsValue({
|
||||
style: "horizontal",
|
||||
category: "custom",
|
||||
labels: [],
|
||||
objects: [{ name: "image", type: "Image", value: "$image" }],
|
||||
});
|
||||
}
|
||||
}, [visible, template, mode, form]);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
setLoading(true);
|
||||
|
||||
console.log("Form values:", values);
|
||||
|
||||
const requestData = {
|
||||
name: values.name,
|
||||
description: values.description,
|
||||
dataType: values.dataType,
|
||||
labelingType: values.labelingType,
|
||||
style: values.style,
|
||||
category: values.category,
|
||||
configuration: {
|
||||
labels: values.labels,
|
||||
objects: values.objects,
|
||||
},
|
||||
};
|
||||
|
||||
console.log("Request data:", requestData);
|
||||
|
||||
let response;
|
||||
if (mode === "create") {
|
||||
response = await createAnnotationTemplateUsingPost(requestData);
|
||||
} else {
|
||||
response = await updateAnnotationTemplateByIdUsingPut(template!.id, requestData);
|
||||
}
|
||||
|
||||
if (response.code === 200) {
|
||||
message.success(`模板${mode === "create" ? "创建" : "更新"}成功`);
|
||||
form.resetFields();
|
||||
onSuccess();
|
||||
} else {
|
||||
message.error(response.message || `模板${mode === "create" ? "创建" : "更新"}失败`);
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (error.errorFields) {
|
||||
message.error("请填写所有必填字段");
|
||||
} else {
|
||||
message.error(`模板${mode === "create" ? "创建" : "更新"}失败`);
|
||||
console.error(error);
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const needsOptions = (type: string) => {
|
||||
return ["Choices", "RectangleLabels", "PolygonLabels", "Labels"].includes(type);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={mode === "create" ? "创建模板" : "编辑模板"}
|
||||
open={visible}
|
||||
onCancel={onCancel}
|
||||
onOk={handleSubmit}
|
||||
confirmLoading={loading}
|
||||
width={900}
|
||||
destroyOnClose
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
style={{ maxHeight: "70vh", overflowY: "auto", paddingRight: 8 }}
|
||||
>
|
||||
<Form.Item
|
||||
label="模板名称"
|
||||
name="name"
|
||||
rules={[{ required: true, message: "请输入模板名称" }]}
|
||||
>
|
||||
<Input placeholder="例如:产品质量分类" maxLength={100} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="描述" name="description">
|
||||
<TextArea
|
||||
placeholder="描述此模板的用途"
|
||||
rows={2}
|
||||
maxLength={500}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Space style={{ width: "100%" }} size="large">
|
||||
<Form.Item
|
||||
label="数据类型"
|
||||
name="dataType"
|
||||
rules={[{ required: true, message: "请选择数据类型" }]}
|
||||
style={{ width: 200 }}
|
||||
>
|
||||
<Select placeholder="选择数据类型">
|
||||
<Option value="image">图像</Option>
|
||||
<Option value="text">文本</Option>
|
||||
<Option value="audio">音频</Option>
|
||||
<Option value="video">视频</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="标注类型"
|
||||
name="labelingType"
|
||||
rules={[{ required: true, message: "请选择标注类型" }]}
|
||||
style={{ width: 220 }}
|
||||
>
|
||||
<Select placeholder="选择标注类型">
|
||||
<Option value="classification">分类</Option>
|
||||
<Option value="object-detection">目标检测</Option>
|
||||
<Option value="segmentation">分割</Option>
|
||||
<Option value="ner">命名实体识别</Option>
|
||||
<Option value="multi-stage">多阶段</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="样式"
|
||||
name="style"
|
||||
style={{ width: 150 }}
|
||||
>
|
||||
<Select>
|
||||
<Option value="horizontal">水平</Option>
|
||||
<Option value="vertical">垂直</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="分类"
|
||||
name="category"
|
||||
style={{ width: 180 }}
|
||||
>
|
||||
<Select>
|
||||
<Option value="computer-vision">计算机视觉</Option>
|
||||
<Option value="nlp">自然语言处理</Option>
|
||||
<Option value="audio">音频</Option>
|
||||
<Option value="quality-control">质量控制</Option>
|
||||
<Option value="custom">自定义</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Space>
|
||||
|
||||
<Divider>数据对象</Divider>
|
||||
|
||||
<Form.List name="objects">
|
||||
{(fields, { add, remove }) => (
|
||||
<>
|
||||
{fields.map((field) => (
|
||||
<Card key={field.key} size="small" style={{ marginBottom: 8 }}>
|
||||
<Space align="start" style={{ width: "100%" }}>
|
||||
<Form.Item
|
||||
{...field}
|
||||
label="名称"
|
||||
name={[field.name, "name"]}
|
||||
rules={[{ required: true, message: "必填" }]}
|
||||
style={{ marginBottom: 0, width: 150 }}
|
||||
>
|
||||
<Input placeholder="例如:image" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
{...field}
|
||||
label="类型"
|
||||
name={[field.name, "type"]}
|
||||
rules={[{ required: true, message: "必填" }]}
|
||||
style={{ marginBottom: 0, width: 150 }}
|
||||
>
|
||||
<TagSelector type="object" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
{...field}
|
||||
label="值"
|
||||
name={[field.name, "value"]}
|
||||
rules={[
|
||||
{ required: true, message: "必填" },
|
||||
{ pattern: /^\$/, message: "必须以 $ 开头" },
|
||||
]}
|
||||
style={{ marginBottom: 0, width: 150 }}
|
||||
>
|
||||
<Input placeholder="$image" />
|
||||
</Form.Item>
|
||||
|
||||
{fields.length > 1 && (
|
||||
<MinusCircleOutlined
|
||||
style={{ marginTop: 30, color: "red" }}
|
||||
onClick={() => remove(field.name)}
|
||||
/>
|
||||
)}
|
||||
</Space>
|
||||
</Card>
|
||||
))}
|
||||
<Button type="dashed" onClick={() => add()} block icon={<PlusOutlined />}>
|
||||
添加对象
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</Form.List>
|
||||
|
||||
<Divider>标签控件</Divider>
|
||||
|
||||
<Form.List name="labels">
|
||||
{(fields, { add, remove }) => (
|
||||
<>
|
||||
{fields.map((field) => (
|
||||
<Card
|
||||
key={field.key}
|
||||
size="small"
|
||||
style={{ marginBottom: 12 }}
|
||||
title={
|
||||
<Space>
|
||||
<span>控件 {fields.indexOf(field) + 1}</span>
|
||||
<Form.Item noStyle shouldUpdate>
|
||||
{() => {
|
||||
const controlType = form.getFieldValue(["labels", field.name, "type"]);
|
||||
const fromName = form.getFieldValue(["labels", field.name, "fromName"]);
|
||||
if (controlType || fromName) {
|
||||
return (
|
||||
<span style={{ fontSize: 12, fontWeight: 'normal', color: '#999' }}>
|
||||
({fromName || '未命名'} - {controlType || '未设置类型'})
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}}
|
||||
</Form.Item>
|
||||
</Space>
|
||||
}
|
||||
extra={
|
||||
<MinusCircleOutlined
|
||||
style={{ color: "red" }}
|
||||
onClick={() => remove(field.name)}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Space direction="vertical" style={{ width: "100%" }} size="middle">
|
||||
{/* Row 1: 控件名称, 标注目标对象, 控件类型 */}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '180px 220px 1fr auto', gap: 12, alignItems: 'flex-end' }}>
|
||||
<Form.Item
|
||||
{...field}
|
||||
label="来源名称"
|
||||
name={[field.name, "fromName"]}
|
||||
rules={[{ required: true, message: "必填" }]}
|
||||
style={{ marginBottom: 0 }}
|
||||
tooltip="此控件的唯一标识符"
|
||||
>
|
||||
<Input placeholder="例如:choice" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
{...field}
|
||||
label="标注目标对象"
|
||||
name={[field.name, "toName"]}
|
||||
rules={[{ required: true, message: "必填" }]}
|
||||
style={{ marginBottom: 0 }}
|
||||
tooltip="选择此控件将标注哪个数据对象"
|
||||
dependencies={['objects']}
|
||||
>
|
||||
<Select placeholder="选择数据对象">
|
||||
{(form.getFieldValue("objects") || []).map((obj: any, idx: number) => (
|
||||
<Option key={idx} value={obj?.name || ''}>
|
||||
{obj?.name || `对象 ${idx + 1}`} ({obj?.type || '未知类型'})
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
{...field}
|
||||
label="控件类型"
|
||||
name={[field.name, "type"]}
|
||||
rules={[{ required: true, message: "必填" }]}
|
||||
style={{ marginBottom: 0 }}
|
||||
>
|
||||
<TagSelector type="control" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
{...field}
|
||||
label=" "
|
||||
name={[field.name, "required"]}
|
||||
valuePropName="checked"
|
||||
style={{ marginBottom: 0 }}
|
||||
>
|
||||
<Checkbox>必填</Checkbox>
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
{/* Row 2: 取值范围定义(添加选项) - Conditionally rendered based on type */}
|
||||
<Form.Item
|
||||
noStyle
|
||||
shouldUpdate={(prevValues, currentValues) => {
|
||||
const prevType = prevValues.labels?.[field.name]?.type;
|
||||
const currType = currentValues.labels?.[field.name]?.type;
|
||||
return prevType !== currType;
|
||||
}}
|
||||
>
|
||||
{({ getFieldValue }) => {
|
||||
const controlType = getFieldValue(["labels", field.name, "type"]);
|
||||
const fieldName = controlType === "Choices" ? "options" : "labels";
|
||||
|
||||
if (needsOptions(controlType)) {
|
||||
return (
|
||||
<Form.Item
|
||||
{...field}
|
||||
label={controlType === "Choices" ? "选项" : "标签"}
|
||||
name={[field.name, fieldName]}
|
||||
rules={[{ required: true, message: "至少需要一个选项" }]}
|
||||
style={{ marginBottom: 0 }}
|
||||
>
|
||||
<Select
|
||||
mode="tags"
|
||||
open={false}
|
||||
placeholder={
|
||||
controlType === "Choices"
|
||||
? "输入选项内容,按回车添加。例如:是、否、不确定"
|
||||
: "输入标签名称,按回车添加。例如:人物、车辆、建筑物"
|
||||
}
|
||||
style={{ width: "100%" }}
|
||||
/>
|
||||
</Form.Item>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}}
|
||||
</Form.Item>
|
||||
|
||||
{/* Row 3: 描述 */}
|
||||
<Form.Item
|
||||
{...field}
|
||||
label="描述"
|
||||
name={[field.name, "description"]}
|
||||
style={{ marginBottom: 0 }}
|
||||
tooltip="向标注人员显示的帮助信息"
|
||||
>
|
||||
<Input placeholder="为标注人员提供此控件的使用说明" maxLength={200} />
|
||||
</Form.Item>
|
||||
</Space>
|
||||
</Card>
|
||||
))}
|
||||
<Button
|
||||
type="dashed"
|
||||
onClick={() =>
|
||||
add({
|
||||
fromName: "",
|
||||
toName: "",
|
||||
type: "Choices",
|
||||
required: false,
|
||||
})
|
||||
}
|
||||
block
|
||||
icon={<PlusOutlined />}
|
||||
>
|
||||
添加标签控件
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</Form.List>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default TemplateForm;
|
||||
import React, { useState, useEffect } from "react";
|
||||
import {
|
||||
Modal,
|
||||
Form,
|
||||
Input,
|
||||
Select,
|
||||
Button,
|
||||
Space,
|
||||
message,
|
||||
Divider,
|
||||
Card,
|
||||
Checkbox,
|
||||
} from "antd";
|
||||
import { PlusOutlined, MinusCircleOutlined } from "@ant-design/icons";
|
||||
import {
|
||||
createAnnotationTemplateUsingPost,
|
||||
updateAnnotationTemplateByIdUsingPut,
|
||||
} from "../annotation.api";
|
||||
import type { AnnotationTemplate } from "../annotation.model";
|
||||
import TagSelector from "./components/TagSelector";
|
||||
|
||||
const { TextArea } = Input;
|
||||
const { Option } = Select;
|
||||
|
||||
interface TemplateFormProps {
|
||||
visible: boolean;
|
||||
mode: "create" | "edit";
|
||||
template?: AnnotationTemplate;
|
||||
onSuccess: () => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
const TemplateForm: React.FC<TemplateFormProps> = ({
|
||||
visible,
|
||||
mode,
|
||||
template,
|
||||
onSuccess,
|
||||
onCancel,
|
||||
}) => {
|
||||
const [form] = Form.useForm();
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (visible && template && mode === "edit") {
|
||||
form.setFieldsValue({
|
||||
name: template.name,
|
||||
description: template.description,
|
||||
dataType: template.dataType,
|
||||
labelingType: template.labelingType,
|
||||
style: template.style,
|
||||
category: template.category,
|
||||
labels: template.configuration.labels,
|
||||
objects: template.configuration.objects,
|
||||
});
|
||||
} else if (visible && mode === "create") {
|
||||
form.resetFields();
|
||||
// Set default values
|
||||
form.setFieldsValue({
|
||||
style: "horizontal",
|
||||
category: "custom",
|
||||
labels: [],
|
||||
objects: [{ name: "image", type: "Image", value: "$image" }],
|
||||
});
|
||||
}
|
||||
}, [visible, template, mode, form]);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
setLoading(true);
|
||||
|
||||
console.log("Form values:", values);
|
||||
|
||||
const requestData = {
|
||||
name: values.name,
|
||||
description: values.description,
|
||||
dataType: values.dataType,
|
||||
labelingType: values.labelingType,
|
||||
style: values.style,
|
||||
category: values.category,
|
||||
configuration: {
|
||||
labels: values.labels,
|
||||
objects: values.objects,
|
||||
},
|
||||
};
|
||||
|
||||
console.log("Request data:", requestData);
|
||||
|
||||
let response;
|
||||
if (mode === "create") {
|
||||
response = await createAnnotationTemplateUsingPost(requestData);
|
||||
} else {
|
||||
response = await updateAnnotationTemplateByIdUsingPut(template!.id, requestData);
|
||||
}
|
||||
|
||||
if (response.code === 200) {
|
||||
message.success(`模板${mode === "create" ? "创建" : "更新"}成功`);
|
||||
form.resetFields();
|
||||
onSuccess();
|
||||
} else {
|
||||
message.error(response.message || `模板${mode === "create" ? "创建" : "更新"}失败`);
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (error.errorFields) {
|
||||
message.error("请填写所有必填字段");
|
||||
} else {
|
||||
message.error(`模板${mode === "create" ? "创建" : "更新"}失败`);
|
||||
console.error(error);
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const needsOptions = (type: string) => {
|
||||
return ["Choices", "RectangleLabels", "PolygonLabels", "Labels"].includes(type);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={mode === "create" ? "创建模板" : "编辑模板"}
|
||||
open={visible}
|
||||
onCancel={onCancel}
|
||||
onOk={handleSubmit}
|
||||
confirmLoading={loading}
|
||||
width={900}
|
||||
destroyOnClose
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
style={{ maxHeight: "70vh", overflowY: "auto", paddingRight: 8 }}
|
||||
>
|
||||
<Form.Item
|
||||
label="模板名称"
|
||||
name="name"
|
||||
rules={[{ required: true, message: "请输入模板名称" }]}
|
||||
>
|
||||
<Input placeholder="例如:产品质量分类" maxLength={100} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="描述" name="description">
|
||||
<TextArea
|
||||
placeholder="描述此模板的用途"
|
||||
rows={2}
|
||||
maxLength={500}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Space style={{ width: "100%" }} size="large">
|
||||
<Form.Item
|
||||
label="数据类型"
|
||||
name="dataType"
|
||||
rules={[{ required: true, message: "请选择数据类型" }]}
|
||||
style={{ width: 200 }}
|
||||
>
|
||||
<Select placeholder="选择数据类型">
|
||||
<Option value="image">图像</Option>
|
||||
<Option value="text">文本</Option>
|
||||
<Option value="audio">音频</Option>
|
||||
<Option value="video">视频</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="标注类型"
|
||||
name="labelingType"
|
||||
rules={[{ required: true, message: "请选择标注类型" }]}
|
||||
style={{ width: 220 }}
|
||||
>
|
||||
<Select placeholder="选择标注类型">
|
||||
<Option value="classification">分类</Option>
|
||||
<Option value="object-detection">目标检测</Option>
|
||||
<Option value="segmentation">分割</Option>
|
||||
<Option value="ner">命名实体识别</Option>
|
||||
<Option value="multi-stage">多阶段</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="样式"
|
||||
name="style"
|
||||
style={{ width: 150 }}
|
||||
>
|
||||
<Select>
|
||||
<Option value="horizontal">水平</Option>
|
||||
<Option value="vertical">垂直</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="分类"
|
||||
name="category"
|
||||
style={{ width: 180 }}
|
||||
>
|
||||
<Select>
|
||||
<Option value="computer-vision">计算机视觉</Option>
|
||||
<Option value="nlp">自然语言处理</Option>
|
||||
<Option value="audio">音频</Option>
|
||||
<Option value="quality-control">质量控制</Option>
|
||||
<Option value="custom">自定义</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Space>
|
||||
|
||||
<Divider>数据对象</Divider>
|
||||
|
||||
<Form.List name="objects">
|
||||
{(fields, { add, remove }) => (
|
||||
<>
|
||||
{fields.map((field) => (
|
||||
<Card key={field.key} size="small" style={{ marginBottom: 8 }}>
|
||||
<Space align="start" style={{ width: "100%" }}>
|
||||
<Form.Item
|
||||
{...field}
|
||||
label="名称"
|
||||
name={[field.name, "name"]}
|
||||
rules={[{ required: true, message: "必填" }]}
|
||||
style={{ marginBottom: 0, width: 150 }}
|
||||
>
|
||||
<Input placeholder="例如:image" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
{...field}
|
||||
label="类型"
|
||||
name={[field.name, "type"]}
|
||||
rules={[{ required: true, message: "必填" }]}
|
||||
style={{ marginBottom: 0, width: 150 }}
|
||||
>
|
||||
<TagSelector type="object" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
{...field}
|
||||
label="值"
|
||||
name={[field.name, "value"]}
|
||||
rules={[
|
||||
{ required: true, message: "必填" },
|
||||
{ pattern: /^\$/, message: "必须以 $ 开头" },
|
||||
]}
|
||||
style={{ marginBottom: 0, width: 150 }}
|
||||
>
|
||||
<Input placeholder="$image" />
|
||||
</Form.Item>
|
||||
|
||||
{fields.length > 1 && (
|
||||
<MinusCircleOutlined
|
||||
style={{ marginTop: 30, color: "red" }}
|
||||
onClick={() => remove(field.name)}
|
||||
/>
|
||||
)}
|
||||
</Space>
|
||||
</Card>
|
||||
))}
|
||||
<Button type="dashed" onClick={() => add()} block icon={<PlusOutlined />}>
|
||||
添加对象
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</Form.List>
|
||||
|
||||
<Divider>标签控件</Divider>
|
||||
|
||||
<Form.List name="labels">
|
||||
{(fields, { add, remove }) => (
|
||||
<>
|
||||
{fields.map((field) => (
|
||||
<Card
|
||||
key={field.key}
|
||||
size="small"
|
||||
style={{ marginBottom: 12 }}
|
||||
title={
|
||||
<Space>
|
||||
<span>控件 {fields.indexOf(field) + 1}</span>
|
||||
<Form.Item noStyle shouldUpdate>
|
||||
{() => {
|
||||
const controlType = form.getFieldValue(["labels", field.name, "type"]);
|
||||
const fromName = form.getFieldValue(["labels", field.name, "fromName"]);
|
||||
if (controlType || fromName) {
|
||||
return (
|
||||
<span style={{ fontSize: 12, fontWeight: 'normal', color: '#999' }}>
|
||||
({fromName || '未命名'} - {controlType || '未设置类型'})
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}}
|
||||
</Form.Item>
|
||||
</Space>
|
||||
}
|
||||
extra={
|
||||
<MinusCircleOutlined
|
||||
style={{ color: "red" }}
|
||||
onClick={() => remove(field.name)}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Space direction="vertical" style={{ width: "100%" }} size="middle">
|
||||
{/* Row 1: 控件名称, 标注目标对象, 控件类型 */}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '180px 220px 1fr auto', gap: 12, alignItems: 'flex-end' }}>
|
||||
<Form.Item
|
||||
{...field}
|
||||
label="来源名称"
|
||||
name={[field.name, "fromName"]}
|
||||
rules={[{ required: true, message: "必填" }]}
|
||||
style={{ marginBottom: 0 }}
|
||||
tooltip="此控件的唯一标识符"
|
||||
>
|
||||
<Input placeholder="例如:choice" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
{...field}
|
||||
label="标注目标对象"
|
||||
name={[field.name, "toName"]}
|
||||
rules={[{ required: true, message: "必填" }]}
|
||||
style={{ marginBottom: 0 }}
|
||||
tooltip="选择此控件将标注哪个数据对象"
|
||||
dependencies={['objects']}
|
||||
>
|
||||
<Select placeholder="选择数据对象">
|
||||
{(form.getFieldValue("objects") || []).map((obj: any, idx: number) => (
|
||||
<Option key={idx} value={obj?.name || ''}>
|
||||
{obj?.name || `对象 ${idx + 1}`} ({obj?.type || '未知类型'})
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
{...field}
|
||||
label="控件类型"
|
||||
name={[field.name, "type"]}
|
||||
rules={[{ required: true, message: "必填" }]}
|
||||
style={{ marginBottom: 0 }}
|
||||
>
|
||||
<TagSelector type="control" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
{...field}
|
||||
label=" "
|
||||
name={[field.name, "required"]}
|
||||
valuePropName="checked"
|
||||
style={{ marginBottom: 0 }}
|
||||
>
|
||||
<Checkbox>必填</Checkbox>
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
{/* Row 2: 取值范围定义(添加选项) - Conditionally rendered based on type */}
|
||||
<Form.Item
|
||||
noStyle
|
||||
shouldUpdate={(prevValues, currentValues) => {
|
||||
const prevType = prevValues.labels?.[field.name]?.type;
|
||||
const currType = currentValues.labels?.[field.name]?.type;
|
||||
return prevType !== currType;
|
||||
}}
|
||||
>
|
||||
{({ getFieldValue }) => {
|
||||
const controlType = getFieldValue(["labels", field.name, "type"]);
|
||||
const fieldName = controlType === "Choices" ? "options" : "labels";
|
||||
|
||||
if (needsOptions(controlType)) {
|
||||
return (
|
||||
<Form.Item
|
||||
{...field}
|
||||
label={controlType === "Choices" ? "选项" : "标签"}
|
||||
name={[field.name, fieldName]}
|
||||
rules={[{ required: true, message: "至少需要一个选项" }]}
|
||||
style={{ marginBottom: 0 }}
|
||||
>
|
||||
<Select
|
||||
mode="tags"
|
||||
open={false}
|
||||
placeholder={
|
||||
controlType === "Choices"
|
||||
? "输入选项内容,按回车添加。例如:是、否、不确定"
|
||||
: "输入标签名称,按回车添加。例如:人物、车辆、建筑物"
|
||||
}
|
||||
style={{ width: "100%" }}
|
||||
/>
|
||||
</Form.Item>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}}
|
||||
</Form.Item>
|
||||
|
||||
{/* Row 3: 描述 */}
|
||||
<Form.Item
|
||||
{...field}
|
||||
label="描述"
|
||||
name={[field.name, "description"]}
|
||||
style={{ marginBottom: 0 }}
|
||||
tooltip="向标注人员显示的帮助信息"
|
||||
>
|
||||
<Input placeholder="为标注人员提供此控件的使用说明" maxLength={200} />
|
||||
</Form.Item>
|
||||
</Space>
|
||||
</Card>
|
||||
))}
|
||||
<Button
|
||||
type="dashed"
|
||||
onClick={() =>
|
||||
add({
|
||||
fromName: "",
|
||||
toName: "",
|
||||
type: "Choices",
|
||||
required: false,
|
||||
})
|
||||
}
|
||||
block
|
||||
icon={<PlusOutlined />}
|
||||
>
|
||||
添加标签控件
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</Form.List>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default TemplateForm;
|
||||
|
||||
@@ -1,311 +1,311 @@
|
||||
import React, { useState } from "react";
|
||||
import {
|
||||
Button,
|
||||
Table,
|
||||
Space,
|
||||
Tag,
|
||||
message,
|
||||
Tooltip,
|
||||
Popconfirm,
|
||||
Card,
|
||||
} from "antd";
|
||||
import {
|
||||
PlusOutlined,
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
EyeOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import type { ColumnsType } from "antd/es/table";
|
||||
import {
|
||||
queryAnnotationTemplatesUsingGet,
|
||||
deleteAnnotationTemplateByIdUsingDelete,
|
||||
} from "../annotation.api";
|
||||
import type { AnnotationTemplate } from "../annotation.model";
|
||||
import TemplateForm from "./TemplateForm.tsx";
|
||||
import TemplateDetail from "./TemplateDetail.tsx";
|
||||
import {SearchControls} from "@/components/SearchControls.tsx";
|
||||
import useFetchData from "@/hooks/useFetchData.ts";
|
||||
import {
|
||||
AnnotationTypeMap,
|
||||
ClassificationMap,
|
||||
DataTypeMap,
|
||||
TemplateTypeMap
|
||||
} from "@/pages/DataAnnotation/annotation.const.tsx";
|
||||
|
||||
const TemplateList: React.FC = () => {
|
||||
const filterOptions = [
|
||||
{
|
||||
key: "category",
|
||||
label: "分类",
|
||||
options: [...Object.values(ClassificationMap)],
|
||||
},
|
||||
{
|
||||
key: "dataType",
|
||||
label: "数据类型",
|
||||
options: [...Object.values(DataTypeMap)],
|
||||
},
|
||||
{
|
||||
key: "labelingType",
|
||||
label: "标注类型",
|
||||
options: [...Object.values(AnnotationTypeMap)],
|
||||
},
|
||||
{
|
||||
key: "builtIn",
|
||||
label: "模板类型",
|
||||
options: [...Object.values(TemplateTypeMap)],
|
||||
},
|
||||
];
|
||||
|
||||
// Modals
|
||||
const [isFormVisible, setIsFormVisible] = useState(false);
|
||||
const [isDetailVisible, setIsDetailVisible] = useState(false);
|
||||
const [selectedTemplate, setSelectedTemplate] = useState<AnnotationTemplate | undefined>();
|
||||
const [formMode, setFormMode] = useState<"create" | "edit">("create");
|
||||
|
||||
const {
|
||||
loading,
|
||||
tableData,
|
||||
pagination,
|
||||
searchParams,
|
||||
setSearchParams,
|
||||
fetchData,
|
||||
handleFiltersChange,
|
||||
handleKeywordChange,
|
||||
} = useFetchData(queryAnnotationTemplatesUsingGet, undefined, undefined, undefined, undefined, 0);
|
||||
|
||||
const handleCreate = () => {
|
||||
setFormMode("create");
|
||||
setSelectedTemplate(undefined);
|
||||
setIsFormVisible(true);
|
||||
};
|
||||
|
||||
const handleEdit = (template: AnnotationTemplate) => {
|
||||
setFormMode("edit");
|
||||
setSelectedTemplate(template);
|
||||
setIsFormVisible(true);
|
||||
};
|
||||
|
||||
const handleView = (template: AnnotationTemplate) => {
|
||||
setSelectedTemplate(template);
|
||||
setIsDetailVisible(true);
|
||||
};
|
||||
|
||||
const handleDelete = async (templateId: string) => {
|
||||
try {
|
||||
const response = await deleteAnnotationTemplateByIdUsingDelete(templateId);
|
||||
if (response.code === 200) {
|
||||
message.success("模板删除成功");
|
||||
fetchData();
|
||||
} else {
|
||||
message.error(response.message || "删除模板失败");
|
||||
}
|
||||
} catch (error) {
|
||||
message.error("删除模板失败");
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFormSuccess = () => {
|
||||
setIsFormVisible(false);
|
||||
fetchData();
|
||||
};
|
||||
|
||||
const getCategoryColor = (category: string) => {
|
||||
const colors: Record<string, string> = {
|
||||
"computer-vision": "blue",
|
||||
"nlp": "green",
|
||||
"audio": "purple",
|
||||
"quality-control": "orange",
|
||||
"custom": "default",
|
||||
};
|
||||
return colors[category] || "default";
|
||||
};
|
||||
|
||||
const columns: ColumnsType<AnnotationTemplate> = [
|
||||
{
|
||||
title: "模板名称",
|
||||
dataIndex: "name",
|
||||
key: "name",
|
||||
width: 200,
|
||||
ellipsis: true,
|
||||
onFilter: (value, record) =>
|
||||
record.name.toLowerCase().includes(value.toString().toLowerCase()) ||
|
||||
(record.description?.toLowerCase().includes(value.toString().toLowerCase()) ?? false),
|
||||
},
|
||||
{
|
||||
title: "描述",
|
||||
dataIndex: "description",
|
||||
key: "description",
|
||||
ellipsis: {
|
||||
showTitle: false,
|
||||
},
|
||||
render: (description: string) => (
|
||||
<Tooltip title={description}>
|
||||
<div
|
||||
style={{
|
||||
display: '-webkit-box',
|
||||
WebkitLineClamp: 2,
|
||||
WebkitBoxOrient: 'vertical',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'normal',
|
||||
lineHeight: '1.5em',
|
||||
maxHeight: '3em',
|
||||
}}
|
||||
>
|
||||
{description}
|
||||
</div>
|
||||
</Tooltip>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "数据类型",
|
||||
dataIndex: "dataType",
|
||||
key: "dataType",
|
||||
width: 120,
|
||||
render: (dataType: string) => (
|
||||
<Tag color="cyan">{dataType}</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "标注类型",
|
||||
dataIndex: "labelingType",
|
||||
key: "labelingType",
|
||||
width: 150,
|
||||
render: (labelingType: string) => (
|
||||
<Tag color="geekblue">{labelingType}</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "分类",
|
||||
dataIndex: "category",
|
||||
key: "category",
|
||||
width: 150,
|
||||
render: (category: string) => (
|
||||
<Tag color={getCategoryColor(category)}>{category}</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "类型",
|
||||
dataIndex: "builtIn",
|
||||
key: "builtIn",
|
||||
width: 100,
|
||||
render: (builtIn: boolean) => (
|
||||
<Tag color={builtIn ? "gold" : "default"}>
|
||||
{builtIn ? "系统内置" : "自定义"}
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "版本",
|
||||
dataIndex: "version",
|
||||
key: "version",
|
||||
width: 80,
|
||||
},
|
||||
{
|
||||
title: "创建时间",
|
||||
dataIndex: "createdAt",
|
||||
key: "createdAt",
|
||||
width: 180,
|
||||
render: (date: string) => new Date(date).toLocaleString(),
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
key: "action",
|
||||
width: 200,
|
||||
fixed: "right",
|
||||
render: (_, record) => (
|
||||
<Space size="small">
|
||||
<Tooltip title="查看详情">
|
||||
<Button
|
||||
type="link"
|
||||
icon={<EyeOutlined />}
|
||||
onClick={() => handleView(record)}
|
||||
/>
|
||||
</Tooltip>
|
||||
{!record.builtIn && (
|
||||
<>
|
||||
<Tooltip title="编辑">
|
||||
<Button
|
||||
type="link"
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => handleEdit(record)}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Popconfirm
|
||||
title="确定要删除这个模板吗?"
|
||||
onConfirm={() => handleDelete(record.id)}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
>
|
||||
<Tooltip title="删除">
|
||||
<Button
|
||||
type="link"
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Popconfirm>
|
||||
</>
|
||||
)}
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
{/* Search, Filters and Buttons in one row */}
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
{/* Left side: Search and Filters */}
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<SearchControls
|
||||
searchTerm={searchParams.keyword}
|
||||
onSearchChange={handleKeywordChange}
|
||||
searchPlaceholder="搜索任务名称、描述"
|
||||
filters={filterOptions}
|
||||
onFiltersChange={handleFiltersChange}
|
||||
showViewToggle={true}
|
||||
onReload={fetchData}
|
||||
onClearFilters={() => setSearchParams({ ...searchParams, filter: {} })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Right side: Create button */}
|
||||
<div className="flex items-center gap-2">
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={handleCreate}>
|
||||
创建模板
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={tableData}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
pagination={pagination}
|
||||
scroll={{ x: 1400, y: "calc(100vh - 24rem)" }}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<TemplateForm
|
||||
visible={isFormVisible}
|
||||
mode={formMode}
|
||||
template={selectedTemplate}
|
||||
onSuccess={handleFormSuccess}
|
||||
onCancel={() => setIsFormVisible(false)}
|
||||
/>
|
||||
|
||||
<TemplateDetail
|
||||
visible={isDetailVisible}
|
||||
template={selectedTemplate}
|
||||
onClose={() => setIsDetailVisible(false)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TemplateList;
|
||||
export { TemplateList };
|
||||
import React, { useState } from "react";
|
||||
import {
|
||||
Button,
|
||||
Table,
|
||||
Space,
|
||||
Tag,
|
||||
message,
|
||||
Tooltip,
|
||||
Popconfirm,
|
||||
Card,
|
||||
} from "antd";
|
||||
import {
|
||||
PlusOutlined,
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
EyeOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import type { ColumnsType } from "antd/es/table";
|
||||
import {
|
||||
queryAnnotationTemplatesUsingGet,
|
||||
deleteAnnotationTemplateByIdUsingDelete,
|
||||
} from "../annotation.api";
|
||||
import type { AnnotationTemplate } from "../annotation.model";
|
||||
import TemplateForm from "./TemplateForm.tsx";
|
||||
import TemplateDetail from "./TemplateDetail.tsx";
|
||||
import {SearchControls} from "@/components/SearchControls.tsx";
|
||||
import useFetchData from "@/hooks/useFetchData.ts";
|
||||
import {
|
||||
AnnotationTypeMap,
|
||||
ClassificationMap,
|
||||
DataTypeMap,
|
||||
TemplateTypeMap
|
||||
} from "@/pages/DataAnnotation/annotation.const.tsx";
|
||||
|
||||
const TemplateList: React.FC = () => {
|
||||
const filterOptions = [
|
||||
{
|
||||
key: "category",
|
||||
label: "分类",
|
||||
options: [...Object.values(ClassificationMap)],
|
||||
},
|
||||
{
|
||||
key: "dataType",
|
||||
label: "数据类型",
|
||||
options: [...Object.values(DataTypeMap)],
|
||||
},
|
||||
{
|
||||
key: "labelingType",
|
||||
label: "标注类型",
|
||||
options: [...Object.values(AnnotationTypeMap)],
|
||||
},
|
||||
{
|
||||
key: "builtIn",
|
||||
label: "模板类型",
|
||||
options: [...Object.values(TemplateTypeMap)],
|
||||
},
|
||||
];
|
||||
|
||||
// Modals
|
||||
const [isFormVisible, setIsFormVisible] = useState(false);
|
||||
const [isDetailVisible, setIsDetailVisible] = useState(false);
|
||||
const [selectedTemplate, setSelectedTemplate] = useState<AnnotationTemplate | undefined>();
|
||||
const [formMode, setFormMode] = useState<"create" | "edit">("create");
|
||||
|
||||
const {
|
||||
loading,
|
||||
tableData,
|
||||
pagination,
|
||||
searchParams,
|
||||
setSearchParams,
|
||||
fetchData,
|
||||
handleFiltersChange,
|
||||
handleKeywordChange,
|
||||
} = useFetchData(queryAnnotationTemplatesUsingGet, undefined, undefined, undefined, undefined, 0);
|
||||
|
||||
const handleCreate = () => {
|
||||
setFormMode("create");
|
||||
setSelectedTemplate(undefined);
|
||||
setIsFormVisible(true);
|
||||
};
|
||||
|
||||
const handleEdit = (template: AnnotationTemplate) => {
|
||||
setFormMode("edit");
|
||||
setSelectedTemplate(template);
|
||||
setIsFormVisible(true);
|
||||
};
|
||||
|
||||
const handleView = (template: AnnotationTemplate) => {
|
||||
setSelectedTemplate(template);
|
||||
setIsDetailVisible(true);
|
||||
};
|
||||
|
||||
const handleDelete = async (templateId: string) => {
|
||||
try {
|
||||
const response = await deleteAnnotationTemplateByIdUsingDelete(templateId);
|
||||
if (response.code === 200) {
|
||||
message.success("模板删除成功");
|
||||
fetchData();
|
||||
} else {
|
||||
message.error(response.message || "删除模板失败");
|
||||
}
|
||||
} catch (error) {
|
||||
message.error("删除模板失败");
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFormSuccess = () => {
|
||||
setIsFormVisible(false);
|
||||
fetchData();
|
||||
};
|
||||
|
||||
const getCategoryColor = (category: string) => {
|
||||
const colors: Record<string, string> = {
|
||||
"computer-vision": "blue",
|
||||
"nlp": "green",
|
||||
"audio": "purple",
|
||||
"quality-control": "orange",
|
||||
"custom": "default",
|
||||
};
|
||||
return colors[category] || "default";
|
||||
};
|
||||
|
||||
const columns: ColumnsType<AnnotationTemplate> = [
|
||||
{
|
||||
title: "模板名称",
|
||||
dataIndex: "name",
|
||||
key: "name",
|
||||
width: 200,
|
||||
ellipsis: true,
|
||||
onFilter: (value, record) =>
|
||||
record.name.toLowerCase().includes(value.toString().toLowerCase()) ||
|
||||
(record.description?.toLowerCase().includes(value.toString().toLowerCase()) ?? false),
|
||||
},
|
||||
{
|
||||
title: "描述",
|
||||
dataIndex: "description",
|
||||
key: "description",
|
||||
ellipsis: {
|
||||
showTitle: false,
|
||||
},
|
||||
render: (description: string) => (
|
||||
<Tooltip title={description}>
|
||||
<div
|
||||
style={{
|
||||
display: '-webkit-box',
|
||||
WebkitLineClamp: 2,
|
||||
WebkitBoxOrient: 'vertical',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'normal',
|
||||
lineHeight: '1.5em',
|
||||
maxHeight: '3em',
|
||||
}}
|
||||
>
|
||||
{description}
|
||||
</div>
|
||||
</Tooltip>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "数据类型",
|
||||
dataIndex: "dataType",
|
||||
key: "dataType",
|
||||
width: 120,
|
||||
render: (dataType: string) => (
|
||||
<Tag color="cyan">{dataType}</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "标注类型",
|
||||
dataIndex: "labelingType",
|
||||
key: "labelingType",
|
||||
width: 150,
|
||||
render: (labelingType: string) => (
|
||||
<Tag color="geekblue">{labelingType}</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "分类",
|
||||
dataIndex: "category",
|
||||
key: "category",
|
||||
width: 150,
|
||||
render: (category: string) => (
|
||||
<Tag color={getCategoryColor(category)}>{category}</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "类型",
|
||||
dataIndex: "builtIn",
|
||||
key: "builtIn",
|
||||
width: 100,
|
||||
render: (builtIn: boolean) => (
|
||||
<Tag color={builtIn ? "gold" : "default"}>
|
||||
{builtIn ? "系统内置" : "自定义"}
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "版本",
|
||||
dataIndex: "version",
|
||||
key: "version",
|
||||
width: 80,
|
||||
},
|
||||
{
|
||||
title: "创建时间",
|
||||
dataIndex: "createdAt",
|
||||
key: "createdAt",
|
||||
width: 180,
|
||||
render: (date: string) => new Date(date).toLocaleString(),
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
key: "action",
|
||||
width: 200,
|
||||
fixed: "right",
|
||||
render: (_, record) => (
|
||||
<Space size="small">
|
||||
<Tooltip title="查看详情">
|
||||
<Button
|
||||
type="link"
|
||||
icon={<EyeOutlined />}
|
||||
onClick={() => handleView(record)}
|
||||
/>
|
||||
</Tooltip>
|
||||
{!record.builtIn && (
|
||||
<>
|
||||
<Tooltip title="编辑">
|
||||
<Button
|
||||
type="link"
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => handleEdit(record)}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Popconfirm
|
||||
title="确定要删除这个模板吗?"
|
||||
onConfirm={() => handleDelete(record.id)}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
>
|
||||
<Tooltip title="删除">
|
||||
<Button
|
||||
type="link"
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Popconfirm>
|
||||
</>
|
||||
)}
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
{/* Search, Filters and Buttons in one row */}
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
{/* Left side: Search and Filters */}
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<SearchControls
|
||||
searchTerm={searchParams.keyword}
|
||||
onSearchChange={handleKeywordChange}
|
||||
searchPlaceholder="搜索任务名称、描述"
|
||||
filters={filterOptions}
|
||||
onFiltersChange={handleFiltersChange}
|
||||
showViewToggle={true}
|
||||
onReload={fetchData}
|
||||
onClearFilters={() => setSearchParams({ ...searchParams, filter: {} })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Right side: Create button */}
|
||||
<div className="flex items-center gap-2">
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={handleCreate}>
|
||||
创建模板
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={tableData}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
pagination={pagination}
|
||||
scroll={{ x: 1400, y: "calc(100vh - 24rem)" }}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<TemplateForm
|
||||
visible={isFormVisible}
|
||||
mode={formMode}
|
||||
template={selectedTemplate}
|
||||
onSuccess={handleFormSuccess}
|
||||
onCancel={() => setIsFormVisible(false)}
|
||||
/>
|
||||
|
||||
<TemplateDetail
|
||||
visible={isDetailVisible}
|
||||
template={selectedTemplate}
|
||||
onClose={() => setIsDetailVisible(false)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TemplateList;
|
||||
export { TemplateList };
|
||||
|
||||
@@ -1,161 +1,161 @@
|
||||
import React, { useState } from "react";
|
||||
import {
|
||||
Card,
|
||||
Button,
|
||||
Space,
|
||||
Row,
|
||||
Col,
|
||||
Drawer,
|
||||
Typography,
|
||||
message,
|
||||
} from "antd";
|
||||
import {
|
||||
PlusOutlined,
|
||||
EyeOutlined,
|
||||
CodeOutlined,
|
||||
AppstoreOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { TagBrowser } from "./components";
|
||||
|
||||
const { Paragraph } = Typography;
|
||||
|
||||
interface VisualTemplateBuilderProps {
|
||||
onSave?: (templateCode: string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Visual Template Builder
|
||||
* Provides a drag-and-drop interface for building Label Studio templates
|
||||
*/
|
||||
const VisualTemplateBuilder: React.FC<VisualTemplateBuilderProps> = ({
|
||||
onSave,
|
||||
}) => {
|
||||
const [drawerVisible, setDrawerVisible] = useState(false);
|
||||
const [previewVisible, setPreviewVisible] = useState(false);
|
||||
const [selectedTags, setSelectedTags] = useState<
|
||||
Array<{ name: string; category: "object" | "control" }>
|
||||
>([]);
|
||||
|
||||
const handleTagSelect = (tagName: string, category: "object" | "control") => {
|
||||
message.info(`选择了 ${category === "object" ? "对象" : "控件"}: ${tagName}`);
|
||||
setSelectedTags([...selectedTags, { name: tagName, category }]);
|
||||
setDrawerVisible(false);
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
// TODO: Generate template XML from selectedTags
|
||||
message.success("模板保存成功");
|
||||
onSave?.("<View><!-- Generated template --></View>");
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ padding: "24px" }}>
|
||||
<Row gutter={16}>
|
||||
<Col span={24}>
|
||||
<Card
|
||||
title="可视化模板构建器"
|
||||
extra={
|
||||
<Space>
|
||||
<Button
|
||||
icon={<AppstoreOutlined />}
|
||||
onClick={() => setDrawerVisible(true)}
|
||||
>
|
||||
浏览标签
|
||||
</Button>
|
||||
<Button
|
||||
icon={<CodeOutlined />}
|
||||
onClick={() => setPreviewVisible(true)}
|
||||
>
|
||||
查看代码
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<EyeOutlined />}
|
||||
onClick={handleSave}
|
||||
>
|
||||
保存模板
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
minHeight: "400px",
|
||||
border: "2px dashed #d9d9d9",
|
||||
borderRadius: "8px",
|
||||
padding: "24px",
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
{selectedTags.length === 0 ? (
|
||||
<div>
|
||||
<Paragraph type="secondary">
|
||||
点击"浏览标签"开始构建您的标注模板
|
||||
</Paragraph>
|
||||
<Button
|
||||
type="dashed"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => setDrawerVisible(true)}
|
||||
>
|
||||
添加标签
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<Space direction="vertical" size="large">
|
||||
{selectedTags.map((tag, index) => (
|
||||
<Card key={index} size="small">
|
||||
<div>
|
||||
{tag.category === "object" ? "对象" : "控件"}: {tag.name}
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</Space>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Drawer
|
||||
title="标签浏览器"
|
||||
placement="right"
|
||||
width={800}
|
||||
open={drawerVisible}
|
||||
onClose={() => setDrawerVisible(false)}
|
||||
>
|
||||
<TagBrowser onTagSelect={handleTagSelect} />
|
||||
</Drawer>
|
||||
|
||||
<Drawer
|
||||
title="模板代码预览"
|
||||
placement="right"
|
||||
width={600}
|
||||
open={previewVisible}
|
||||
onClose={() => setPreviewVisible(false)}
|
||||
>
|
||||
<pre
|
||||
style={{
|
||||
background: "#f5f5f5",
|
||||
padding: "16px",
|
||||
borderRadius: "4px",
|
||||
overflow: "auto",
|
||||
}}
|
||||
>
|
||||
<code>
|
||||
{`<View>
|
||||
<!-- 根据选择的标签生成的模板代码 -->
|
||||
${selectedTags
|
||||
.map(
|
||||
(tag) =>
|
||||
`<${tag.name}${tag.category === "object" ? ' name="obj" value="$data"' : ' name="ctrl" toName="obj"'} />`
|
||||
)
|
||||
.join("\n ")}
|
||||
</View>`}
|
||||
</code>
|
||||
</pre>
|
||||
</Drawer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default VisualTemplateBuilder;
|
||||
import React, { useState } from "react";
|
||||
import {
|
||||
Card,
|
||||
Button,
|
||||
Space,
|
||||
Row,
|
||||
Col,
|
||||
Drawer,
|
||||
Typography,
|
||||
message,
|
||||
} from "antd";
|
||||
import {
|
||||
PlusOutlined,
|
||||
EyeOutlined,
|
||||
CodeOutlined,
|
||||
AppstoreOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { TagBrowser } from "./components";
|
||||
|
||||
const { Paragraph } = Typography;
|
||||
|
||||
interface VisualTemplateBuilderProps {
|
||||
onSave?: (templateCode: string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Visual Template Builder
|
||||
* Provides a drag-and-drop interface for building Label Studio templates
|
||||
*/
|
||||
const VisualTemplateBuilder: React.FC<VisualTemplateBuilderProps> = ({
|
||||
onSave,
|
||||
}) => {
|
||||
const [drawerVisible, setDrawerVisible] = useState(false);
|
||||
const [previewVisible, setPreviewVisible] = useState(false);
|
||||
const [selectedTags, setSelectedTags] = useState<
|
||||
Array<{ name: string; category: "object" | "control" }>
|
||||
>([]);
|
||||
|
||||
const handleTagSelect = (tagName: string, category: "object" | "control") => {
|
||||
message.info(`选择了 ${category === "object" ? "对象" : "控件"}: ${tagName}`);
|
||||
setSelectedTags([...selectedTags, { name: tagName, category }]);
|
||||
setDrawerVisible(false);
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
// TODO: Generate template XML from selectedTags
|
||||
message.success("模板保存成功");
|
||||
onSave?.("<View><!-- Generated template --></View>");
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ padding: "24px" }}>
|
||||
<Row gutter={16}>
|
||||
<Col span={24}>
|
||||
<Card
|
||||
title="可视化模板构建器"
|
||||
extra={
|
||||
<Space>
|
||||
<Button
|
||||
icon={<AppstoreOutlined />}
|
||||
onClick={() => setDrawerVisible(true)}
|
||||
>
|
||||
浏览标签
|
||||
</Button>
|
||||
<Button
|
||||
icon={<CodeOutlined />}
|
||||
onClick={() => setPreviewVisible(true)}
|
||||
>
|
||||
查看代码
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<EyeOutlined />}
|
||||
onClick={handleSave}
|
||||
>
|
||||
保存模板
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
minHeight: "400px",
|
||||
border: "2px dashed #d9d9d9",
|
||||
borderRadius: "8px",
|
||||
padding: "24px",
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
{selectedTags.length === 0 ? (
|
||||
<div>
|
||||
<Paragraph type="secondary">
|
||||
点击"浏览标签"开始构建您的标注模板
|
||||
</Paragraph>
|
||||
<Button
|
||||
type="dashed"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => setDrawerVisible(true)}
|
||||
>
|
||||
添加标签
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<Space direction="vertical" size="large">
|
||||
{selectedTags.map((tag, index) => (
|
||||
<Card key={index} size="small">
|
||||
<div>
|
||||
{tag.category === "object" ? "对象" : "控件"}: {tag.name}
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</Space>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Drawer
|
||||
title="标签浏览器"
|
||||
placement="right"
|
||||
width={800}
|
||||
open={drawerVisible}
|
||||
onClose={() => setDrawerVisible(false)}
|
||||
>
|
||||
<TagBrowser onTagSelect={handleTagSelect} />
|
||||
</Drawer>
|
||||
|
||||
<Drawer
|
||||
title="模板代码预览"
|
||||
placement="right"
|
||||
width={600}
|
||||
open={previewVisible}
|
||||
onClose={() => setPreviewVisible(false)}
|
||||
>
|
||||
<pre
|
||||
style={{
|
||||
background: "#f5f5f5",
|
||||
padding: "16px",
|
||||
borderRadius: "4px",
|
||||
overflow: "auto",
|
||||
}}
|
||||
>
|
||||
<code>
|
||||
{`<View>
|
||||
<!-- 根据选择的标签生成的模板代码 -->
|
||||
${selectedTags
|
||||
.map(
|
||||
(tag) =>
|
||||
`<${tag.name}${tag.category === "object" ? ' name="obj" value="$data"' : ' name="ctrl" toName="obj"'} />`
|
||||
)
|
||||
.join("\n ")}
|
||||
</View>`}
|
||||
</code>
|
||||
</pre>
|
||||
</Drawer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default VisualTemplateBuilder;
|
||||
|
||||
@@ -1,260 +1,260 @@
|
||||
import React from "react";
|
||||
import { Card, Tabs, List, Tag, Typography, Space, Empty, Spin } from "antd";
|
||||
import {
|
||||
AppstoreOutlined,
|
||||
ControlOutlined,
|
||||
InfoCircleOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { useTagConfig } from "../../../../hooks/useTagConfig";
|
||||
import {
|
||||
getControlDisplayName,
|
||||
getObjectDisplayName,
|
||||
getControlGroups,
|
||||
} from "../../annotation.tagconfig";
|
||||
import type { TagOption } from "../../annotation.tagconfig";
|
||||
|
||||
const { Title, Paragraph, Text } = Typography;
|
||||
|
||||
interface TagBrowserProps {
|
||||
onTagSelect?: (tagName: string, category: "object" | "control") => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tag Browser Component
|
||||
* Displays all available Label Studio tags in a browsable interface
|
||||
*/
|
||||
const TagBrowser: React.FC<TagBrowserProps> = ({ onTagSelect }) => {
|
||||
const { config, objectOptions, controlOptions, loading, error } =
|
||||
useTagConfig();
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Card>
|
||||
<div style={{ textAlign: "center", padding: "40px" }}>
|
||||
<Spin size="large" />
|
||||
<div style={{ marginTop: 16 }}>加载标签配置...</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Card>
|
||||
<Empty
|
||||
description={
|
||||
<div>
|
||||
<div>{error}</div>
|
||||
<Text type="secondary">无法加载标签配置</Text>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
const renderObjectList = () => (
|
||||
<List
|
||||
grid={{ gutter: 16, xs: 1, sm: 2, md: 2, lg: 3, xl: 3, xxl: 4 }}
|
||||
dataSource={objectOptions}
|
||||
renderItem={(item: TagOption) => {
|
||||
const objConfig = config?.objects[item.value];
|
||||
return (
|
||||
<List.Item>
|
||||
<Card
|
||||
hoverable
|
||||
size="small"
|
||||
onClick={() => onTagSelect?.(item.value, "object")}
|
||||
style={{ height: "100%" }}
|
||||
>
|
||||
<Space direction="vertical" size="small" style={{ width: "100%" }}>
|
||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
||||
<Text strong>{getObjectDisplayName(item.value)}</Text>
|
||||
<Tag color="blue"><{item.value}></Tag>
|
||||
</div>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
{item.description}
|
||||
</Text>
|
||||
{objConfig && (
|
||||
<div style={{ marginTop: 8 }}>
|
||||
<Text style={{ fontSize: 11, color: "#8c8c8c" }}>
|
||||
必需属性:{" "}
|
||||
{objConfig.required_attrs.join(", ") || "无"}
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
</Space>
|
||||
</Card>
|
||||
</List.Item>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const renderControlsByGroup = () => {
|
||||
const groups = getControlGroups();
|
||||
|
||||
return (
|
||||
<Tabs
|
||||
defaultActiveKey="classification"
|
||||
items={Object.entries(groups).map(([groupKey, groupConfig]) => {
|
||||
const groupControls = controlOptions.filter((opt: TagOption) =>
|
||||
groupConfig.controls.includes(opt.value)
|
||||
);
|
||||
|
||||
return {
|
||||
key: groupKey,
|
||||
label: groupConfig.label,
|
||||
children: (
|
||||
<List
|
||||
grid={{ gutter: 16, xs: 1, sm: 2, md: 2, lg: 3, xl: 3, xxl: 4 }}
|
||||
dataSource={groupControls}
|
||||
locale={{ emptyText: "此分组暂无控件" }}
|
||||
renderItem={(item: TagOption) => {
|
||||
const ctrlConfig = config?.controls[item.value];
|
||||
return (
|
||||
<List.Item>
|
||||
<Card
|
||||
hoverable
|
||||
size="small"
|
||||
onClick={() => onTagSelect?.(item.value, "control")}
|
||||
style={{ height: "100%" }}
|
||||
>
|
||||
<Space direction="vertical" size="small" style={{ width: "100%" }}>
|
||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
||||
<Text strong>
|
||||
{getControlDisplayName(item.value)}
|
||||
</Text>
|
||||
<Tag color="green"><{item.value}></Tag>
|
||||
</div>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
{item.description}
|
||||
</Text>
|
||||
{ctrlConfig && (
|
||||
<Space
|
||||
size={4}
|
||||
wrap
|
||||
style={{ marginTop: 8 }}
|
||||
>
|
||||
{ctrlConfig.requires_children && (
|
||||
<Tag
|
||||
color="orange"
|
||||
style={{ fontSize: 10, margin: 0 }}
|
||||
>
|
||||
需要 <{ctrlConfig.child_tag}>
|
||||
</Tag>
|
||||
)}
|
||||
{ctrlConfig.required_attrs.includes("toName") && (
|
||||
<Tag
|
||||
color="purple"
|
||||
style={{ fontSize: 10, margin: 0 }}
|
||||
>
|
||||
绑定对象
|
||||
</Tag>
|
||||
)}
|
||||
</Space>
|
||||
)}
|
||||
</Space>
|
||||
</Card>
|
||||
</List.Item>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
),
|
||||
};
|
||||
})}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<Tabs
|
||||
defaultActiveKey="controls"
|
||||
items={[
|
||||
{
|
||||
key: "controls",
|
||||
label: (
|
||||
<span>
|
||||
<ControlOutlined />
|
||||
控件标签 ({controlOptions.length})
|
||||
</span>
|
||||
),
|
||||
children: renderControlsByGroup(),
|
||||
},
|
||||
{
|
||||
key: "objects",
|
||||
label: (
|
||||
<span>
|
||||
<AppstoreOutlined />
|
||||
数据对象 ({objectOptions.length})
|
||||
</span>
|
||||
),
|
||||
children: renderObjectList(),
|
||||
},
|
||||
{
|
||||
key: "help",
|
||||
label: (
|
||||
<span>
|
||||
<InfoCircleOutlined />
|
||||
使用说明
|
||||
</span>
|
||||
),
|
||||
children: (
|
||||
<div style={{ padding: "16px" }}>
|
||||
<Title level={4}>Label Studio 标签配置说明</Title>
|
||||
<Paragraph>
|
||||
标注模板由两类标签组成:
|
||||
</Paragraph>
|
||||
<ul>
|
||||
<li>
|
||||
<Text strong>数据对象标签</Text>:定义要标注的数据类型(如图像、文本、音频等)
|
||||
</li>
|
||||
<li>
|
||||
<Text strong>控件标签</Text>:定义标注工具和交互方式(如矩形框、分类选项、文本输入等)
|
||||
</li>
|
||||
</ul>
|
||||
<Title level={5} style={{ marginTop: 24 }}>
|
||||
基本结构
|
||||
</Title>
|
||||
<Paragraph>
|
||||
<pre style={{ background: "#f5f5f5", padding: 12, borderRadius: 4 }}>
|
||||
{`<View>
|
||||
<!-- 数据对象 -->
|
||||
<Image name="image" value="$image" />
|
||||
|
||||
<!-- 控件 -->
|
||||
<RectangleLabels name="label" toName="image">
|
||||
<Label value="人物" />
|
||||
<Label value="车辆" />
|
||||
</RectangleLabels>
|
||||
</View>`}
|
||||
</pre>
|
||||
</Paragraph>
|
||||
<Title level={5} style={{ marginTop: 24 }}>
|
||||
属性说明
|
||||
</Title>
|
||||
<ul>
|
||||
<li>
|
||||
<Text code>name</Text>:控件的唯一标识符
|
||||
</li>
|
||||
<li>
|
||||
<Text code>toName</Text>:指向要标注的数据对象的 name
|
||||
</li>
|
||||
<li>
|
||||
<Text code>value</Text>:数据源字段,以 $ 开头(如 $image, $text)
|
||||
</li>
|
||||
<li>
|
||||
<Text code>required</Text>:是否必填(可选)
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default TagBrowser;
|
||||
import React from "react";
|
||||
import { Card, Tabs, List, Tag, Typography, Space, Empty, Spin } from "antd";
|
||||
import {
|
||||
AppstoreOutlined,
|
||||
ControlOutlined,
|
||||
InfoCircleOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { useTagConfig } from "../../../../hooks/useTagConfig";
|
||||
import {
|
||||
getControlDisplayName,
|
||||
getObjectDisplayName,
|
||||
getControlGroups,
|
||||
} from "../../annotation.tagconfig";
|
||||
import type { TagOption } from "../../annotation.tagconfig";
|
||||
|
||||
const { Title, Paragraph, Text } = Typography;
|
||||
|
||||
interface TagBrowserProps {
|
||||
onTagSelect?: (tagName: string, category: "object" | "control") => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tag Browser Component
|
||||
* Displays all available Label Studio tags in a browsable interface
|
||||
*/
|
||||
const TagBrowser: React.FC<TagBrowserProps> = ({ onTagSelect }) => {
|
||||
const { config, objectOptions, controlOptions, loading, error } =
|
||||
useTagConfig();
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Card>
|
||||
<div style={{ textAlign: "center", padding: "40px" }}>
|
||||
<Spin size="large" />
|
||||
<div style={{ marginTop: 16 }}>加载标签配置...</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Card>
|
||||
<Empty
|
||||
description={
|
||||
<div>
|
||||
<div>{error}</div>
|
||||
<Text type="secondary">无法加载标签配置</Text>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
const renderObjectList = () => (
|
||||
<List
|
||||
grid={{ gutter: 16, xs: 1, sm: 2, md: 2, lg: 3, xl: 3, xxl: 4 }}
|
||||
dataSource={objectOptions}
|
||||
renderItem={(item: TagOption) => {
|
||||
const objConfig = config?.objects[item.value];
|
||||
return (
|
||||
<List.Item>
|
||||
<Card
|
||||
hoverable
|
||||
size="small"
|
||||
onClick={() => onTagSelect?.(item.value, "object")}
|
||||
style={{ height: "100%" }}
|
||||
>
|
||||
<Space direction="vertical" size="small" style={{ width: "100%" }}>
|
||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
||||
<Text strong>{getObjectDisplayName(item.value)}</Text>
|
||||
<Tag color="blue"><{item.value}></Tag>
|
||||
</div>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
{item.description}
|
||||
</Text>
|
||||
{objConfig && (
|
||||
<div style={{ marginTop: 8 }}>
|
||||
<Text style={{ fontSize: 11, color: "#8c8c8c" }}>
|
||||
必需属性:{" "}
|
||||
{objConfig.required_attrs.join(", ") || "无"}
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
</Space>
|
||||
</Card>
|
||||
</List.Item>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const renderControlsByGroup = () => {
|
||||
const groups = getControlGroups();
|
||||
|
||||
return (
|
||||
<Tabs
|
||||
defaultActiveKey="classification"
|
||||
items={Object.entries(groups).map(([groupKey, groupConfig]) => {
|
||||
const groupControls = controlOptions.filter((opt: TagOption) =>
|
||||
groupConfig.controls.includes(opt.value)
|
||||
);
|
||||
|
||||
return {
|
||||
key: groupKey,
|
||||
label: groupConfig.label,
|
||||
children: (
|
||||
<List
|
||||
grid={{ gutter: 16, xs: 1, sm: 2, md: 2, lg: 3, xl: 3, xxl: 4 }}
|
||||
dataSource={groupControls}
|
||||
locale={{ emptyText: "此分组暂无控件" }}
|
||||
renderItem={(item: TagOption) => {
|
||||
const ctrlConfig = config?.controls[item.value];
|
||||
return (
|
||||
<List.Item>
|
||||
<Card
|
||||
hoverable
|
||||
size="small"
|
||||
onClick={() => onTagSelect?.(item.value, "control")}
|
||||
style={{ height: "100%" }}
|
||||
>
|
||||
<Space direction="vertical" size="small" style={{ width: "100%" }}>
|
||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
||||
<Text strong>
|
||||
{getControlDisplayName(item.value)}
|
||||
</Text>
|
||||
<Tag color="green"><{item.value}></Tag>
|
||||
</div>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
{item.description}
|
||||
</Text>
|
||||
{ctrlConfig && (
|
||||
<Space
|
||||
size={4}
|
||||
wrap
|
||||
style={{ marginTop: 8 }}
|
||||
>
|
||||
{ctrlConfig.requires_children && (
|
||||
<Tag
|
||||
color="orange"
|
||||
style={{ fontSize: 10, margin: 0 }}
|
||||
>
|
||||
需要 <{ctrlConfig.child_tag}>
|
||||
</Tag>
|
||||
)}
|
||||
{ctrlConfig.required_attrs.includes("toName") && (
|
||||
<Tag
|
||||
color="purple"
|
||||
style={{ fontSize: 10, margin: 0 }}
|
||||
>
|
||||
绑定对象
|
||||
</Tag>
|
||||
)}
|
||||
</Space>
|
||||
)}
|
||||
</Space>
|
||||
</Card>
|
||||
</List.Item>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
),
|
||||
};
|
||||
})}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<Tabs
|
||||
defaultActiveKey="controls"
|
||||
items={[
|
||||
{
|
||||
key: "controls",
|
||||
label: (
|
||||
<span>
|
||||
<ControlOutlined />
|
||||
控件标签 ({controlOptions.length})
|
||||
</span>
|
||||
),
|
||||
children: renderControlsByGroup(),
|
||||
},
|
||||
{
|
||||
key: "objects",
|
||||
label: (
|
||||
<span>
|
||||
<AppstoreOutlined />
|
||||
数据对象 ({objectOptions.length})
|
||||
</span>
|
||||
),
|
||||
children: renderObjectList(),
|
||||
},
|
||||
{
|
||||
key: "help",
|
||||
label: (
|
||||
<span>
|
||||
<InfoCircleOutlined />
|
||||
使用说明
|
||||
</span>
|
||||
),
|
||||
children: (
|
||||
<div style={{ padding: "16px" }}>
|
||||
<Title level={4}>Label Studio 标签配置说明</Title>
|
||||
<Paragraph>
|
||||
标注模板由两类标签组成:
|
||||
</Paragraph>
|
||||
<ul>
|
||||
<li>
|
||||
<Text strong>数据对象标签</Text>:定义要标注的数据类型(如图像、文本、音频等)
|
||||
</li>
|
||||
<li>
|
||||
<Text strong>控件标签</Text>:定义标注工具和交互方式(如矩形框、分类选项、文本输入等)
|
||||
</li>
|
||||
</ul>
|
||||
<Title level={5} style={{ marginTop: 24 }}>
|
||||
基本结构
|
||||
</Title>
|
||||
<Paragraph>
|
||||
<pre style={{ background: "#f5f5f5", padding: 12, borderRadius: 4 }}>
|
||||
{`<View>
|
||||
<!-- 数据对象 -->
|
||||
<Image name="image" value="$image" />
|
||||
|
||||
<!-- 控件 -->
|
||||
<RectangleLabels name="label" toName="image">
|
||||
<Label value="人物" />
|
||||
<Label value="车辆" />
|
||||
</RectangleLabels>
|
||||
</View>`}
|
||||
</pre>
|
||||
</Paragraph>
|
||||
<Title level={5} style={{ marginTop: 24 }}>
|
||||
属性说明
|
||||
</Title>
|
||||
<ul>
|
||||
<li>
|
||||
<Text code>name</Text>:控件的唯一标识符
|
||||
</li>
|
||||
<li>
|
||||
<Text code>toName</Text>:指向要标注的数据对象的 name
|
||||
</li>
|
||||
<li>
|
||||
<Text code>value</Text>:数据源字段,以 $ 开头(如 $image, $text)
|
||||
</li>
|
||||
<li>
|
||||
<Text code>required</Text>:是否必填(可选)
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default TagBrowser;
|
||||
|
||||
@@ -1,301 +1,301 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Select, Tooltip, Spin, Collapse, Tag, Space } from "antd";
|
||||
import { InfoCircleOutlined } from "@ant-design/icons";
|
||||
import { getTagConfigUsingGet } from "../../annotation.api";
|
||||
import type {
|
||||
LabelStudioTagConfig,
|
||||
TagOption,
|
||||
} from "../../annotation.tagconfig";
|
||||
import {
|
||||
parseTagConfig,
|
||||
getControlDisplayName,
|
||||
getObjectDisplayName,
|
||||
getControlGroups,
|
||||
} from "../../annotation.tagconfig";
|
||||
|
||||
const { Option, OptGroup } = Select;
|
||||
|
||||
interface TagSelectorProps {
|
||||
value?: string;
|
||||
onChange?: (value: string) => void;
|
||||
type: "object" | "control";
|
||||
placeholder?: string;
|
||||
style?: React.CSSProperties;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tag Selector Component
|
||||
* Dynamically fetches and displays available Label Studio tags from backend config
|
||||
*/
|
||||
const TagSelector: React.FC<TagSelectorProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
type,
|
||||
placeholder,
|
||||
style,
|
||||
disabled,
|
||||
}) => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [tagOptions, setTagOptions] = useState<TagOption[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchTagConfig();
|
||||
}, []);
|
||||
|
||||
const fetchTagConfig = async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const response = await getTagConfigUsingGet();
|
||||
if (response.code === 200 && response.data) {
|
||||
const config: LabelStudioTagConfig = response.data;
|
||||
const { objectOptions, controlOptions } = parseTagConfig(config);
|
||||
|
||||
if (type === "object") {
|
||||
setTagOptions(objectOptions);
|
||||
} else {
|
||||
setTagOptions(controlOptions);
|
||||
}
|
||||
} else {
|
||||
setError(response.message || "获取标签配置失败");
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error("Failed to fetch tag config:", err);
|
||||
setError("加载标签配置时出错");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Select
|
||||
placeholder="加载中..."
|
||||
style={style}
|
||||
disabled
|
||||
suffixIcon={<Spin size="small" />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Tooltip title={error}>
|
||||
<Select
|
||||
placeholder="加载失败,点击重试"
|
||||
style={style}
|
||||
disabled={disabled}
|
||||
status="error"
|
||||
onClick={() => fetchTagConfig()}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
// Group controls by usage pattern
|
||||
if (type === "control") {
|
||||
const groups = getControlGroups();
|
||||
const groupedOptions: Record<string, TagOption[]> = {};
|
||||
const ungroupedOptions: TagOption[] = [];
|
||||
|
||||
// Group the controls
|
||||
Object.entries(groups).forEach(([groupKey, groupConfig]) => {
|
||||
groupedOptions[groupKey] = tagOptions.filter((opt) =>
|
||||
groupConfig.controls.includes(opt.value)
|
||||
);
|
||||
});
|
||||
|
||||
// Find ungrouped controls
|
||||
const allGroupedControls = new Set(
|
||||
Object.values(groups).flatMap((g) => g.controls)
|
||||
);
|
||||
tagOptions.forEach((opt) => {
|
||||
if (!allGroupedControls.has(opt.value)) {
|
||||
ungroupedOptions.push(opt);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<Select
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder={placeholder || "选择控件类型"}
|
||||
style={style}
|
||||
disabled={disabled}
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
>
|
||||
{Object.entries(groups).map(([groupKey, groupConfig]) => {
|
||||
const options = groupedOptions[groupKey];
|
||||
if (options.length === 0) return null;
|
||||
|
||||
return (
|
||||
<OptGroup key={groupKey} label={groupConfig.label}>
|
||||
{options.map((opt) => (
|
||||
<Option key={opt.value} value={opt.value} label={opt.label}>
|
||||
<div className="flex items-center justify-between">
|
||||
<span>{getControlDisplayName(opt.value)}</span>
|
||||
<Tooltip title={opt.description}>
|
||||
<InfoCircleOutlined
|
||||
style={{ color: "#8c8c8c", fontSize: 12 }}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</Option>
|
||||
))}
|
||||
</OptGroup>
|
||||
);
|
||||
})}
|
||||
{ungroupedOptions.length > 0 && (
|
||||
<OptGroup label="其他">
|
||||
{ungroupedOptions.map((opt) => (
|
||||
<Option key={opt.value} value={opt.value} label={opt.label}>
|
||||
<div className="flex items-center justify-between">
|
||||
<span>{getControlDisplayName(opt.value)}</span>
|
||||
<Tooltip title={opt.description}>
|
||||
<InfoCircleOutlined
|
||||
style={{ color: "#8c8c8c", fontSize: 12 }}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</Option>
|
||||
))}
|
||||
</OptGroup>
|
||||
)}
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
|
||||
// Objects selector (no grouping)
|
||||
return (
|
||||
<Select
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder={placeholder || "选择数据对象类型"}
|
||||
style={style}
|
||||
disabled={disabled}
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
>
|
||||
{tagOptions.map((opt) => (
|
||||
<Option key={opt.value} value={opt.value} label={opt.label}>
|
||||
<div className="flex items-center justify-between">
|
||||
<span>{getObjectDisplayName(opt.value)}</span>
|
||||
<Tooltip title={opt.description}>
|
||||
<InfoCircleOutlined style={{ color: "#8c8c8c", fontSize: 12 }} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
);
|
||||
};
|
||||
|
||||
export default TagSelector;
|
||||
|
||||
/**
|
||||
* Tag Info Panel Component
|
||||
* Displays detailed information about a selected tag
|
||||
*/
|
||||
interface TagInfoPanelProps {
|
||||
tagConfig: LabelStudioTagConfig | null;
|
||||
tagType: string;
|
||||
category: "object" | "control";
|
||||
}
|
||||
|
||||
export const TagInfoPanel: React.FC<TagInfoPanelProps> = ({
|
||||
tagConfig,
|
||||
tagType,
|
||||
category,
|
||||
}) => {
|
||||
if (!tagConfig || !tagType) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const config =
|
||||
category === "object"
|
||||
? tagConfig.objects[tagType]
|
||||
: tagConfig.controls[tagType];
|
||||
|
||||
if (!config) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Collapse
|
||||
size="small"
|
||||
items={[
|
||||
{
|
||||
key: "1",
|
||||
label: "标签配置详情",
|
||||
children: (
|
||||
<Space direction="vertical" size="small" style={{ width: "100%" }}>
|
||||
<div>
|
||||
<strong>描述:</strong>
|
||||
{config.description}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<strong>必需属性:</strong>
|
||||
<div style={{ marginTop: 4 }}>
|
||||
{config.required_attrs.map((attr: string) => (
|
||||
<Tag key={attr} color="red">
|
||||
{attr}
|
||||
</Tag>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{config.optional_attrs &&
|
||||
Object.keys(config.optional_attrs).length > 0 && (
|
||||
<div>
|
||||
<strong>可选属性:</strong>
|
||||
<div style={{ marginTop: 4 }}>
|
||||
{Object.entries(config.optional_attrs).map(
|
||||
([attrName, attrConfig]: [string, any]) => (
|
||||
<Tooltip
|
||||
key={attrName}
|
||||
title={
|
||||
<div>
|
||||
{attrConfig.description && (
|
||||
<div>{attrConfig.description}</div>
|
||||
)}
|
||||
{attrConfig.type && (
|
||||
<div>类型: {attrConfig.type}</div>
|
||||
)}
|
||||
{attrConfig.default !== undefined && (
|
||||
<div>默认值: {String(attrConfig.default)}</div>
|
||||
)}
|
||||
{attrConfig.values && (
|
||||
<div>
|
||||
可选值: {attrConfig.values.join(", ")}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Tag color="blue" style={{ cursor: "help" }}>
|
||||
{attrName}
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{config.requires_children && (
|
||||
<div>
|
||||
<strong>子元素:</strong>
|
||||
<Tag color="green">需要 <{config.child_tag}></Tag>
|
||||
</div>
|
||||
)}
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Select, Tooltip, Spin, Collapse, Tag, Space } from "antd";
|
||||
import { InfoCircleOutlined } from "@ant-design/icons";
|
||||
import { getTagConfigUsingGet } from "../../annotation.api";
|
||||
import type {
|
||||
LabelStudioTagConfig,
|
||||
TagOption,
|
||||
} from "../../annotation.tagconfig";
|
||||
import {
|
||||
parseTagConfig,
|
||||
getControlDisplayName,
|
||||
getObjectDisplayName,
|
||||
getControlGroups,
|
||||
} from "../../annotation.tagconfig";
|
||||
|
||||
const { Option, OptGroup } = Select;
|
||||
|
||||
interface TagSelectorProps {
|
||||
value?: string;
|
||||
onChange?: (value: string) => void;
|
||||
type: "object" | "control";
|
||||
placeholder?: string;
|
||||
style?: React.CSSProperties;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tag Selector Component
|
||||
* Dynamically fetches and displays available Label Studio tags from backend config
|
||||
*/
|
||||
const TagSelector: React.FC<TagSelectorProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
type,
|
||||
placeholder,
|
||||
style,
|
||||
disabled,
|
||||
}) => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [tagOptions, setTagOptions] = useState<TagOption[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchTagConfig();
|
||||
}, []);
|
||||
|
||||
const fetchTagConfig = async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const response = await getTagConfigUsingGet();
|
||||
if (response.code === 200 && response.data) {
|
||||
const config: LabelStudioTagConfig = response.data;
|
||||
const { objectOptions, controlOptions } = parseTagConfig(config);
|
||||
|
||||
if (type === "object") {
|
||||
setTagOptions(objectOptions);
|
||||
} else {
|
||||
setTagOptions(controlOptions);
|
||||
}
|
||||
} else {
|
||||
setError(response.message || "获取标签配置失败");
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error("Failed to fetch tag config:", err);
|
||||
setError("加载标签配置时出错");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Select
|
||||
placeholder="加载中..."
|
||||
style={style}
|
||||
disabled
|
||||
suffixIcon={<Spin size="small" />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Tooltip title={error}>
|
||||
<Select
|
||||
placeholder="加载失败,点击重试"
|
||||
style={style}
|
||||
disabled={disabled}
|
||||
status="error"
|
||||
onClick={() => fetchTagConfig()}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
// Group controls by usage pattern
|
||||
if (type === "control") {
|
||||
const groups = getControlGroups();
|
||||
const groupedOptions: Record<string, TagOption[]> = {};
|
||||
const ungroupedOptions: TagOption[] = [];
|
||||
|
||||
// Group the controls
|
||||
Object.entries(groups).forEach(([groupKey, groupConfig]) => {
|
||||
groupedOptions[groupKey] = tagOptions.filter((opt) =>
|
||||
groupConfig.controls.includes(opt.value)
|
||||
);
|
||||
});
|
||||
|
||||
// Find ungrouped controls
|
||||
const allGroupedControls = new Set(
|
||||
Object.values(groups).flatMap((g) => g.controls)
|
||||
);
|
||||
tagOptions.forEach((opt) => {
|
||||
if (!allGroupedControls.has(opt.value)) {
|
||||
ungroupedOptions.push(opt);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<Select
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder={placeholder || "选择控件类型"}
|
||||
style={style}
|
||||
disabled={disabled}
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
>
|
||||
{Object.entries(groups).map(([groupKey, groupConfig]) => {
|
||||
const options = groupedOptions[groupKey];
|
||||
if (options.length === 0) return null;
|
||||
|
||||
return (
|
||||
<OptGroup key={groupKey} label={groupConfig.label}>
|
||||
{options.map((opt) => (
|
||||
<Option key={opt.value} value={opt.value} label={opt.label}>
|
||||
<div className="flex items-center justify-between">
|
||||
<span>{getControlDisplayName(opt.value)}</span>
|
||||
<Tooltip title={opt.description}>
|
||||
<InfoCircleOutlined
|
||||
style={{ color: "#8c8c8c", fontSize: 12 }}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</Option>
|
||||
))}
|
||||
</OptGroup>
|
||||
);
|
||||
})}
|
||||
{ungroupedOptions.length > 0 && (
|
||||
<OptGroup label="其他">
|
||||
{ungroupedOptions.map((opt) => (
|
||||
<Option key={opt.value} value={opt.value} label={opt.label}>
|
||||
<div className="flex items-center justify-between">
|
||||
<span>{getControlDisplayName(opt.value)}</span>
|
||||
<Tooltip title={opt.description}>
|
||||
<InfoCircleOutlined
|
||||
style={{ color: "#8c8c8c", fontSize: 12 }}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</Option>
|
||||
))}
|
||||
</OptGroup>
|
||||
)}
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
|
||||
// Objects selector (no grouping)
|
||||
return (
|
||||
<Select
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder={placeholder || "选择数据对象类型"}
|
||||
style={style}
|
||||
disabled={disabled}
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
>
|
||||
{tagOptions.map((opt) => (
|
||||
<Option key={opt.value} value={opt.value} label={opt.label}>
|
||||
<div className="flex items-center justify-between">
|
||||
<span>{getObjectDisplayName(opt.value)}</span>
|
||||
<Tooltip title={opt.description}>
|
||||
<InfoCircleOutlined style={{ color: "#8c8c8c", fontSize: 12 }} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
);
|
||||
};
|
||||
|
||||
export default TagSelector;
|
||||
|
||||
/**
|
||||
* Tag Info Panel Component
|
||||
* Displays detailed information about a selected tag
|
||||
*/
|
||||
interface TagInfoPanelProps {
|
||||
tagConfig: LabelStudioTagConfig | null;
|
||||
tagType: string;
|
||||
category: "object" | "control";
|
||||
}
|
||||
|
||||
export const TagInfoPanel: React.FC<TagInfoPanelProps> = ({
|
||||
tagConfig,
|
||||
tagType,
|
||||
category,
|
||||
}) => {
|
||||
if (!tagConfig || !tagType) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const config =
|
||||
category === "object"
|
||||
? tagConfig.objects[tagType]
|
||||
: tagConfig.controls[tagType];
|
||||
|
||||
if (!config) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Collapse
|
||||
size="small"
|
||||
items={[
|
||||
{
|
||||
key: "1",
|
||||
label: "标签配置详情",
|
||||
children: (
|
||||
<Space direction="vertical" size="small" style={{ width: "100%" }}>
|
||||
<div>
|
||||
<strong>描述:</strong>
|
||||
{config.description}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<strong>必需属性:</strong>
|
||||
<div style={{ marginTop: 4 }}>
|
||||
{config.required_attrs.map((attr: string) => (
|
||||
<Tag key={attr} color="red">
|
||||
{attr}
|
||||
</Tag>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{config.optional_attrs &&
|
||||
Object.keys(config.optional_attrs).length > 0 && (
|
||||
<div>
|
||||
<strong>可选属性:</strong>
|
||||
<div style={{ marginTop: 4 }}>
|
||||
{Object.entries(config.optional_attrs).map(
|
||||
([attrName, attrConfig]: [string, any]) => (
|
||||
<Tooltip
|
||||
key={attrName}
|
||||
title={
|
||||
<div>
|
||||
{attrConfig.description && (
|
||||
<div>{attrConfig.description}</div>
|
||||
)}
|
||||
{attrConfig.type && (
|
||||
<div>类型: {attrConfig.type}</div>
|
||||
)}
|
||||
{attrConfig.default !== undefined && (
|
||||
<div>默认值: {String(attrConfig.default)}</div>
|
||||
)}
|
||||
{attrConfig.values && (
|
||||
<div>
|
||||
可选值: {attrConfig.values.join(", ")}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Tag color="blue" style={{ cursor: "help" }}>
|
||||
{attrName}
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{config.requires_children && (
|
||||
<div>
|
||||
<strong>子元素:</strong>
|
||||
<Tag color="green">需要 <{config.child_tag}></Tag>
|
||||
</div>
|
||||
)}
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export { default as TagSelector } from "./TagSelector";
|
||||
export { default as TagBrowser } from "./TagBrowser";
|
||||
export { TagInfoPanel } from "./TagSelector";
|
||||
export { default as TagSelector } from "./TagSelector";
|
||||
export { default as TagBrowser } from "./TagBrowser";
|
||||
export { TagInfoPanel } from "./TagSelector";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export { default as TemplateList } from "./TemplateList";
|
||||
export { default as TemplateForm } from "./TemplateForm";
|
||||
export { default as TemplateDetail } from "./TemplateDetail";
|
||||
export { TagBrowser, TagSelector, TagInfoPanel } from "./components";
|
||||
export { default as TemplateList } from "./TemplateList";
|
||||
export { default as TemplateForm } from "./TemplateForm";
|
||||
export { default as TemplateDetail } from "./TemplateDetail";
|
||||
export { TagBrowser, TagSelector, TagInfoPanel } from "./components";
|
||||
|
||||
@@ -1,50 +1,50 @@
|
||||
import { get, post, put, del } from "@/utils/request";
|
||||
|
||||
// 标注任务管理相关接口
|
||||
export function queryAnnotationTasksUsingGet(params?: any) {
|
||||
return get("/api/annotation/project", params);
|
||||
}
|
||||
|
||||
export function createAnnotationTaskUsingPost(data: any) {
|
||||
return post("/api/annotation/project", data);
|
||||
}
|
||||
|
||||
export function syncAnnotationTaskUsingPost(data: any) {
|
||||
return post(`/api/annotation/task/sync`, data);
|
||||
}
|
||||
|
||||
export function deleteAnnotationTaskByIdUsingDelete(mappingId: string) {
|
||||
// Backend expects mapping UUID as path parameter
|
||||
return del(`/api/annotation/project/${mappingId}`);
|
||||
}
|
||||
|
||||
export function loginAnnotationUsingGet(mappingId: string) {
|
||||
return get("/api/annotation/project/${mappingId}/login");
|
||||
}
|
||||
|
||||
// 标签配置管理
|
||||
export function getTagConfigUsingGet() {
|
||||
return get("/api/annotation/tags/config");
|
||||
}
|
||||
|
||||
// 标注模板管理
|
||||
export function queryAnnotationTemplatesUsingGet(params?: any) {
|
||||
return get("/api/annotation/template", params);
|
||||
}
|
||||
|
||||
export function createAnnotationTemplateUsingPost(data: any) {
|
||||
return post("/api/annotation/template", data);
|
||||
}
|
||||
|
||||
export function updateAnnotationTemplateByIdUsingPut(
|
||||
templateId: string | number,
|
||||
data: any
|
||||
) {
|
||||
return put(`/api/annotation/template/${templateId}`, data);
|
||||
}
|
||||
|
||||
export function deleteAnnotationTemplateByIdUsingDelete(
|
||||
templateId: string | number
|
||||
) {
|
||||
return del(`/api/annotation/template/${templateId}`);
|
||||
}
|
||||
import { get, post, put, del } from "@/utils/request";
|
||||
|
||||
// 标注任务管理相关接口
|
||||
export function queryAnnotationTasksUsingGet(params?: any) {
|
||||
return get("/api/annotation/project", params);
|
||||
}
|
||||
|
||||
export function createAnnotationTaskUsingPost(data: any) {
|
||||
return post("/api/annotation/project", data);
|
||||
}
|
||||
|
||||
export function syncAnnotationTaskUsingPost(data: any) {
|
||||
return post(`/api/annotation/task/sync`, data);
|
||||
}
|
||||
|
||||
export function deleteAnnotationTaskByIdUsingDelete(mappingId: string) {
|
||||
// Backend expects mapping UUID as path parameter
|
||||
return del(`/api/annotation/project/${mappingId}`);
|
||||
}
|
||||
|
||||
export function loginAnnotationUsingGet(mappingId: string) {
|
||||
return get("/api/annotation/project/${mappingId}/login");
|
||||
}
|
||||
|
||||
// 标签配置管理
|
||||
export function getTagConfigUsingGet() {
|
||||
return get("/api/annotation/tags/config");
|
||||
}
|
||||
|
||||
// 标注模板管理
|
||||
export function queryAnnotationTemplatesUsingGet(params?: any) {
|
||||
return get("/api/annotation/template", params);
|
||||
}
|
||||
|
||||
export function createAnnotationTemplateUsingPost(data: any) {
|
||||
return post("/api/annotation/template", data);
|
||||
}
|
||||
|
||||
export function updateAnnotationTemplateByIdUsingPut(
|
||||
templateId: string | number,
|
||||
data: any
|
||||
) {
|
||||
return put(`/api/annotation/template/${templateId}`, data);
|
||||
}
|
||||
|
||||
export function deleteAnnotationTemplateByIdUsingDelete(
|
||||
templateId: string | number
|
||||
) {
|
||||
return del(`/api/annotation/template/${templateId}`);
|
||||
}
|
||||
|
||||
@@ -1,140 +1,140 @@
|
||||
import { StickyNote } from "lucide-react";
|
||||
import {AnnotationTaskStatus, AnnotationType, Classification, DataType, TemplateType} from "./annotation.model";
|
||||
import {
|
||||
CheckCircleOutlined,
|
||||
ClockCircleOutlined,
|
||||
CloseCircleOutlined,
|
||||
} from "@ant-design/icons";
|
||||
|
||||
export const AnnotationTaskStatusMap = {
|
||||
[AnnotationTaskStatus.ACTIVE]: {
|
||||
label: "活跃",
|
||||
value: AnnotationTaskStatus.ACTIVE,
|
||||
color: "#409f17ff",
|
||||
icon: <CheckCircleOutlined />,
|
||||
},
|
||||
[AnnotationTaskStatus.PROCESSING]: {
|
||||
label: "处理中",
|
||||
value: AnnotationTaskStatus.PROCESSING,
|
||||
color: "#2673e5",
|
||||
icon: <ClockCircleOutlined />,
|
||||
},
|
||||
[AnnotationTaskStatus.INACTIVE]: {
|
||||
label: "未激活",
|
||||
value: AnnotationTaskStatus.INACTIVE,
|
||||
color: "#4f4444ff",
|
||||
icon: <CloseCircleOutlined />,
|
||||
},
|
||||
};
|
||||
|
||||
export function mapAnnotationTask(task: any) {
|
||||
// Normalize labeling project id from possible backend field names
|
||||
const labelingProjId = task?.labelingProjId || task?.labelingProjectId || task?.projId || task?.labeling_project_id || "";
|
||||
|
||||
const statsArray = task?.statistics
|
||||
? [
|
||||
{ label: "准确率", value: task.statistics.accuracy ?? "-" },
|
||||
{ label: "平均时长", value: task.statistics.averageTime ?? "-" },
|
||||
{ label: "待复核", value: task.statistics.reviewCount ?? "-" },
|
||||
]
|
||||
: [];
|
||||
|
||||
return {
|
||||
...task,
|
||||
id: task.id,
|
||||
// provide consistent field for components
|
||||
labelingProjId,
|
||||
projId: labelingProjId,
|
||||
name: task.name,
|
||||
description: task.description || "",
|
||||
datasetName: task.datasetName || task.dataset_name || "-",
|
||||
createdAt: task.createdAt || task.created_at || "-",
|
||||
updatedAt: task.updatedAt || task.updated_at || "-",
|
||||
icon: <StickyNote />,
|
||||
iconColor: "bg-blue-100",
|
||||
status: {
|
||||
label:
|
||||
task.status === "completed"
|
||||
? "已完成"
|
||||
: task.status === "processing"
|
||||
? "进行中"
|
||||
: task.status === "skipped"
|
||||
? "已跳过"
|
||||
: "待开始",
|
||||
color: "bg-blue-100",
|
||||
},
|
||||
statistics: statsArray,
|
||||
};
|
||||
}
|
||||
|
||||
export const DataTypeMap = {
|
||||
[DataType.TEXT]: {
|
||||
label: "文本",
|
||||
value: DataType.TEXT
|
||||
},
|
||||
[DataType.IMAGE]: {
|
||||
label: "图片",
|
||||
value: DataType.IMAGE
|
||||
},
|
||||
[DataType.AUDIO]: {
|
||||
label: "音频",
|
||||
value: DataType.AUDIO
|
||||
},
|
||||
[DataType.VIDEO]: {
|
||||
label: "视频",
|
||||
value: DataType.VIDEO
|
||||
},
|
||||
}
|
||||
|
||||
export const ClassificationMap = {
|
||||
[Classification.COMPUTER_VERSION]: {
|
||||
label: "计算机视觉",
|
||||
value: Classification.COMPUTER_VERSION
|
||||
},
|
||||
[Classification.NLP]: {
|
||||
label: "自然语言处理",
|
||||
value: Classification.NLP
|
||||
},
|
||||
[Classification.AUDIO]: {
|
||||
label: "音频",
|
||||
value: Classification.AUDIO
|
||||
},
|
||||
[Classification.QUALITY_CONTROL]: {
|
||||
label: "质量控制",
|
||||
value: Classification.QUALITY_CONTROL
|
||||
},
|
||||
[Classification.CUSTOM]: {
|
||||
label: "自定义",
|
||||
value: Classification.CUSTOM
|
||||
},
|
||||
}
|
||||
|
||||
export const AnnotationTypeMap = {
|
||||
[AnnotationType.CLASSIFICATION]: {
|
||||
label: "分类",
|
||||
value: AnnotationType.CLASSIFICATION
|
||||
},
|
||||
[AnnotationType.OBJECT_DETECTION]: {
|
||||
label: "目标检测",
|
||||
value: AnnotationType.OBJECT_DETECTION
|
||||
},
|
||||
[AnnotationType.SEGMENTATION]: {
|
||||
label: "分割",
|
||||
value: AnnotationType.SEGMENTATION
|
||||
},
|
||||
[AnnotationType.NER]: {
|
||||
label: "命名实体识别",
|
||||
value: AnnotationType.NER
|
||||
},
|
||||
}
|
||||
|
||||
export const TemplateTypeMap = {
|
||||
[TemplateType.SYSTEM]: {
|
||||
label: "系统内置",
|
||||
value: TemplateType.SYSTEM
|
||||
},
|
||||
[TemplateType.CUSTOM]: {
|
||||
label: "自定义",
|
||||
value: TemplateType.CUSTOM
|
||||
},
|
||||
import { StickyNote } from "lucide-react";
|
||||
import {AnnotationTaskStatus, AnnotationType, Classification, DataType, TemplateType} from "./annotation.model";
|
||||
import {
|
||||
CheckCircleOutlined,
|
||||
ClockCircleOutlined,
|
||||
CloseCircleOutlined,
|
||||
} from "@ant-design/icons";
|
||||
|
||||
export const AnnotationTaskStatusMap = {
|
||||
[AnnotationTaskStatus.ACTIVE]: {
|
||||
label: "活跃",
|
||||
value: AnnotationTaskStatus.ACTIVE,
|
||||
color: "#409f17ff",
|
||||
icon: <CheckCircleOutlined />,
|
||||
},
|
||||
[AnnotationTaskStatus.PROCESSING]: {
|
||||
label: "处理中",
|
||||
value: AnnotationTaskStatus.PROCESSING,
|
||||
color: "#2673e5",
|
||||
icon: <ClockCircleOutlined />,
|
||||
},
|
||||
[AnnotationTaskStatus.INACTIVE]: {
|
||||
label: "未激活",
|
||||
value: AnnotationTaskStatus.INACTIVE,
|
||||
color: "#4f4444ff",
|
||||
icon: <CloseCircleOutlined />,
|
||||
},
|
||||
};
|
||||
|
||||
export function mapAnnotationTask(task: any) {
|
||||
// Normalize labeling project id from possible backend field names
|
||||
const labelingProjId = task?.labelingProjId || task?.labelingProjectId || task?.projId || task?.labeling_project_id || "";
|
||||
|
||||
const statsArray = task?.statistics
|
||||
? [
|
||||
{ label: "准确率", value: task.statistics.accuracy ?? "-" },
|
||||
{ label: "平均时长", value: task.statistics.averageTime ?? "-" },
|
||||
{ label: "待复核", value: task.statistics.reviewCount ?? "-" },
|
||||
]
|
||||
: [];
|
||||
|
||||
return {
|
||||
...task,
|
||||
id: task.id,
|
||||
// provide consistent field for components
|
||||
labelingProjId,
|
||||
projId: labelingProjId,
|
||||
name: task.name,
|
||||
description: task.description || "",
|
||||
datasetName: task.datasetName || task.dataset_name || "-",
|
||||
createdAt: task.createdAt || task.created_at || "-",
|
||||
updatedAt: task.updatedAt || task.updated_at || "-",
|
||||
icon: <StickyNote />,
|
||||
iconColor: "bg-blue-100",
|
||||
status: {
|
||||
label:
|
||||
task.status === "completed"
|
||||
? "已完成"
|
||||
: task.status === "processing"
|
||||
? "进行中"
|
||||
: task.status === "skipped"
|
||||
? "已跳过"
|
||||
: "待开始",
|
||||
color: "bg-blue-100",
|
||||
},
|
||||
statistics: statsArray,
|
||||
};
|
||||
}
|
||||
|
||||
export const DataTypeMap = {
|
||||
[DataType.TEXT]: {
|
||||
label: "文本",
|
||||
value: DataType.TEXT
|
||||
},
|
||||
[DataType.IMAGE]: {
|
||||
label: "图片",
|
||||
value: DataType.IMAGE
|
||||
},
|
||||
[DataType.AUDIO]: {
|
||||
label: "音频",
|
||||
value: DataType.AUDIO
|
||||
},
|
||||
[DataType.VIDEO]: {
|
||||
label: "视频",
|
||||
value: DataType.VIDEO
|
||||
},
|
||||
}
|
||||
|
||||
export const ClassificationMap = {
|
||||
[Classification.COMPUTER_VERSION]: {
|
||||
label: "计算机视觉",
|
||||
value: Classification.COMPUTER_VERSION
|
||||
},
|
||||
[Classification.NLP]: {
|
||||
label: "自然语言处理",
|
||||
value: Classification.NLP
|
||||
},
|
||||
[Classification.AUDIO]: {
|
||||
label: "音频",
|
||||
value: Classification.AUDIO
|
||||
},
|
||||
[Classification.QUALITY_CONTROL]: {
|
||||
label: "质量控制",
|
||||
value: Classification.QUALITY_CONTROL
|
||||
},
|
||||
[Classification.CUSTOM]: {
|
||||
label: "自定义",
|
||||
value: Classification.CUSTOM
|
||||
},
|
||||
}
|
||||
|
||||
export const AnnotationTypeMap = {
|
||||
[AnnotationType.CLASSIFICATION]: {
|
||||
label: "分类",
|
||||
value: AnnotationType.CLASSIFICATION
|
||||
},
|
||||
[AnnotationType.OBJECT_DETECTION]: {
|
||||
label: "目标检测",
|
||||
value: AnnotationType.OBJECT_DETECTION
|
||||
},
|
||||
[AnnotationType.SEGMENTATION]: {
|
||||
label: "分割",
|
||||
value: AnnotationType.SEGMENTATION
|
||||
},
|
||||
[AnnotationType.NER]: {
|
||||
label: "命名实体识别",
|
||||
value: AnnotationType.NER
|
||||
},
|
||||
}
|
||||
|
||||
export const TemplateTypeMap = {
|
||||
[TemplateType.SYSTEM]: {
|
||||
label: "系统内置",
|
||||
value: TemplateType.SYSTEM
|
||||
},
|
||||
[TemplateType.CUSTOM]: {
|
||||
label: "自定义",
|
||||
value: TemplateType.CUSTOM
|
||||
},
|
||||
}
|
||||
@@ -1,107 +1,107 @@
|
||||
import type { DatasetType } from "@/pages/DataManagement/dataset.model";
|
||||
|
||||
export enum AnnotationTaskStatus {
|
||||
ACTIVE = "active",
|
||||
INACTIVE = "inactive",
|
||||
PROCESSING = "processing",
|
||||
COMPLETED = "completed",
|
||||
SKIPPED = "skipped",
|
||||
}
|
||||
|
||||
export interface AnnotationTask {
|
||||
id: string;
|
||||
name: string;
|
||||
labelingProjId: string;
|
||||
datasetId: string;
|
||||
|
||||
annotationCount: number;
|
||||
|
||||
description?: string;
|
||||
assignedTo?: string;
|
||||
progress: number;
|
||||
statistics: {
|
||||
accuracy: number;
|
||||
averageTime: number;
|
||||
reviewCount: number;
|
||||
};
|
||||
status: AnnotationTaskStatus;
|
||||
totalDataCount: number;
|
||||
type: DatasetType;
|
||||
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
// 标注模板相关类型
|
||||
export interface LabelDefinition {
|
||||
fromName: string;
|
||||
toName: string;
|
||||
type: string;
|
||||
options?: string[];
|
||||
labels?: string[];
|
||||
required?: boolean;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface ObjectDefinition {
|
||||
name: string;
|
||||
type: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface TemplateConfiguration {
|
||||
labels: LabelDefinition[];
|
||||
objects: ObjectDefinition[];
|
||||
metadata?: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface AnnotationTemplate {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
dataType: string;
|
||||
labelingType: string;
|
||||
configuration: TemplateConfiguration;
|
||||
labelConfig?: string;
|
||||
style: string;
|
||||
category: string;
|
||||
builtIn: boolean;
|
||||
version: string;
|
||||
createdAt: string;
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
export interface AnnotationTemplateListResponse {
|
||||
content: AnnotationTemplate[];
|
||||
total: number;
|
||||
page: number;
|
||||
size: number;
|
||||
totalPages: number;
|
||||
}
|
||||
|
||||
export enum DataType {
|
||||
TEXT = "text",
|
||||
IMAGE = "image",
|
||||
AUDIO = "audio",
|
||||
VIDEO = "video",
|
||||
}
|
||||
|
||||
export enum Classification {
|
||||
COMPUTER_VERSION = "computer-vision",
|
||||
NLP = "nlp",
|
||||
AUDIO = "audio",
|
||||
QUALITY_CONTROL = "quality-control",
|
||||
CUSTOM = "custom"
|
||||
}
|
||||
|
||||
export enum AnnotationType {
|
||||
CLASSIFICATION = "classification",
|
||||
OBJECT_DETECTION = "object-detection",
|
||||
SEGMENTATION = "segmentation",
|
||||
NER = "ner"
|
||||
}
|
||||
|
||||
export enum TemplateType {
|
||||
SYSTEM = "true",
|
||||
CUSTOM = "false"
|
||||
}
|
||||
import type { DatasetType } from "@/pages/DataManagement/dataset.model";
|
||||
|
||||
export enum AnnotationTaskStatus {
|
||||
ACTIVE = "active",
|
||||
INACTIVE = "inactive",
|
||||
PROCESSING = "processing",
|
||||
COMPLETED = "completed",
|
||||
SKIPPED = "skipped",
|
||||
}
|
||||
|
||||
export interface AnnotationTask {
|
||||
id: string;
|
||||
name: string;
|
||||
labelingProjId: string;
|
||||
datasetId: string;
|
||||
|
||||
annotationCount: number;
|
||||
|
||||
description?: string;
|
||||
assignedTo?: string;
|
||||
progress: number;
|
||||
statistics: {
|
||||
accuracy: number;
|
||||
averageTime: number;
|
||||
reviewCount: number;
|
||||
};
|
||||
status: AnnotationTaskStatus;
|
||||
totalDataCount: number;
|
||||
type: DatasetType;
|
||||
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
// 标注模板相关类型
|
||||
export interface LabelDefinition {
|
||||
fromName: string;
|
||||
toName: string;
|
||||
type: string;
|
||||
options?: string[];
|
||||
labels?: string[];
|
||||
required?: boolean;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface ObjectDefinition {
|
||||
name: string;
|
||||
type: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface TemplateConfiguration {
|
||||
labels: LabelDefinition[];
|
||||
objects: ObjectDefinition[];
|
||||
metadata?: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface AnnotationTemplate {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
dataType: string;
|
||||
labelingType: string;
|
||||
configuration: TemplateConfiguration;
|
||||
labelConfig?: string;
|
||||
style: string;
|
||||
category: string;
|
||||
builtIn: boolean;
|
||||
version: string;
|
||||
createdAt: string;
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
export interface AnnotationTemplateListResponse {
|
||||
content: AnnotationTemplate[];
|
||||
total: number;
|
||||
page: number;
|
||||
size: number;
|
||||
totalPages: number;
|
||||
}
|
||||
|
||||
export enum DataType {
|
||||
TEXT = "text",
|
||||
IMAGE = "image",
|
||||
AUDIO = "audio",
|
||||
VIDEO = "video",
|
||||
}
|
||||
|
||||
export enum Classification {
|
||||
COMPUTER_VERSION = "computer-vision",
|
||||
NLP = "nlp",
|
||||
AUDIO = "audio",
|
||||
QUALITY_CONTROL = "quality-control",
|
||||
CUSTOM = "custom"
|
||||
}
|
||||
|
||||
export enum AnnotationType {
|
||||
CLASSIFICATION = "classification",
|
||||
OBJECT_DETECTION = "object-detection",
|
||||
SEGMENTATION = "segmentation",
|
||||
NER = "ner"
|
||||
}
|
||||
|
||||
export enum TemplateType {
|
||||
SYSTEM = "true",
|
||||
CUSTOM = "false"
|
||||
}
|
||||
|
||||
@@ -1,187 +1,187 @@
|
||||
/**
|
||||
* Label Studio Tag Configuration Types
|
||||
* Corresponds to runtime/datamate-python/app/module/annotation/config/label_studio_tags.yaml
|
||||
*/
|
||||
|
||||
export interface TagAttributeConfig {
|
||||
type?: "boolean" | "number" | "string";
|
||||
values?: string[];
|
||||
default?: any;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface TagConfig {
|
||||
description: string;
|
||||
required_attrs: string[];
|
||||
optional_attrs?: Record<string, TagAttributeConfig>;
|
||||
requires_children?: boolean;
|
||||
child_tag?: string;
|
||||
child_required_attrs?: string[];
|
||||
category?: string; // e.g., "labeling" or "layout" for controls; "image", "text", etc. for objects
|
||||
}
|
||||
|
||||
export interface LabelStudioTagConfig {
|
||||
objects: Record<string, TagConfig>;
|
||||
controls: Record<string, TagConfig>;
|
||||
}
|
||||
|
||||
/**
|
||||
* UI-friendly representation of a tag for selection
|
||||
*/
|
||||
export interface TagOption {
|
||||
value: string;
|
||||
label: string;
|
||||
description: string;
|
||||
category: "object" | "control";
|
||||
requiresChildren: boolean;
|
||||
childTag?: string;
|
||||
requiredAttrs: string[];
|
||||
optionalAttrs?: Record<string, TagAttributeConfig>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert backend tag config to frontend tag options
|
||||
* @param config - The full tag configuration from backend
|
||||
* @param includeLabelingOnly - If true, only include controls with category="labeling" (default: true)
|
||||
*/
|
||||
export function parseTagConfig(
|
||||
config: LabelStudioTagConfig,
|
||||
includeLabelingOnly: boolean = true
|
||||
): {
|
||||
objectOptions: TagOption[];
|
||||
controlOptions: TagOption[];
|
||||
} {
|
||||
const objectOptions: TagOption[] = Object.entries(config.objects).map(
|
||||
([key, value]) => ({
|
||||
value: key,
|
||||
label: key,
|
||||
description: value.description,
|
||||
category: "object" as const,
|
||||
requiresChildren: value.requires_children || false,
|
||||
childTag: value.child_tag,
|
||||
requiredAttrs: value.required_attrs,
|
||||
optionalAttrs: value.optional_attrs,
|
||||
})
|
||||
);
|
||||
|
||||
const controlOptions: TagOption[] = Object.entries(config.controls)
|
||||
.filter(([_, value]) => {
|
||||
// If includeLabelingOnly is true, filter out layout controls
|
||||
if (includeLabelingOnly) {
|
||||
return value.category === "labeling";
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.map(([key, value]) => ({
|
||||
value: key,
|
||||
label: key,
|
||||
description: value.description,
|
||||
category: "control" as const,
|
||||
requiresChildren: value.requires_children || false,
|
||||
childTag: value.child_tag,
|
||||
requiredAttrs: value.required_attrs,
|
||||
optionalAttrs: value.optional_attrs,
|
||||
}));
|
||||
|
||||
return { objectOptions, controlOptions };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user-friendly display name for control types
|
||||
*/
|
||||
export function getControlDisplayName(controlType: string): string {
|
||||
const displayNames: Record<string, string> = {
|
||||
Choices: "选项 (单选/多选)",
|
||||
RectangleLabels: "矩形框",
|
||||
PolygonLabels: "多边形",
|
||||
Labels: "标签",
|
||||
TextArea: "文本区域",
|
||||
Rating: "评分",
|
||||
Taxonomy: "分类树",
|
||||
Ranker: "排序",
|
||||
List: "列表",
|
||||
BrushLabels: "画笔分割",
|
||||
EllipseLabels: "椭圆",
|
||||
KeyPointLabels: "关键点",
|
||||
Rectangle: "矩形",
|
||||
Polygon: "多边形",
|
||||
Ellipse: "椭圆",
|
||||
KeyPoint: "关键点",
|
||||
Brush: "画笔",
|
||||
Number: "数字输入",
|
||||
DateTime: "日期时间",
|
||||
Relation: "关系",
|
||||
Relations: "关系组",
|
||||
Pairwise: "成对比较",
|
||||
};
|
||||
|
||||
return displayNames[controlType] || controlType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user-friendly display name for object types
|
||||
*/
|
||||
export function getObjectDisplayName(objectType: string): string {
|
||||
const displayNames: Record<string, string> = {
|
||||
Image: "图像",
|
||||
Text: "文本",
|
||||
Audio: "音频",
|
||||
Video: "视频",
|
||||
HyperText: "HTML内容",
|
||||
PDF: "PDF文档",
|
||||
Markdown: "Markdown内容",
|
||||
Paragraphs: "段落",
|
||||
Table: "表格",
|
||||
AudioPlus: "高级音频",
|
||||
Timeseries: "时间序列",
|
||||
Vector: "向量数据",
|
||||
Chat: "对话数据",
|
||||
};
|
||||
|
||||
return displayNames[objectType] || objectType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Group control types by common usage patterns
|
||||
*/
|
||||
export function getControlGroups(): Record<
|
||||
string,
|
||||
{ label: string; controls: string[] }
|
||||
> {
|
||||
return {
|
||||
classification: {
|
||||
label: "分类标注",
|
||||
controls: ["Choices", "Taxonomy", "Labels", "Rating"],
|
||||
},
|
||||
detection: {
|
||||
label: "目标检测",
|
||||
controls: [
|
||||
"RectangleLabels",
|
||||
"PolygonLabels",
|
||||
"EllipseLabels",
|
||||
"KeyPointLabels",
|
||||
"Rectangle",
|
||||
"Polygon",
|
||||
"Ellipse",
|
||||
"KeyPoint",
|
||||
],
|
||||
},
|
||||
segmentation: {
|
||||
label: "分割标注",
|
||||
controls: ["BrushLabels", "Brush", "BitmaskLabels", "MagicWand"],
|
||||
},
|
||||
text: {
|
||||
label: "文本输入",
|
||||
controls: ["TextArea", "Number", "DateTime"],
|
||||
},
|
||||
other: {
|
||||
label: "其他",
|
||||
controls: [
|
||||
"TimeseriesLabels",
|
||||
"VectorLabels",
|
||||
"ParagraphLabels",
|
||||
"VideoRectangle",
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Label Studio Tag Configuration Types
|
||||
* Corresponds to runtime/datamate-python/app/module/annotation/config/label_studio_tags.yaml
|
||||
*/
|
||||
|
||||
export interface TagAttributeConfig {
|
||||
type?: "boolean" | "number" | "string";
|
||||
values?: string[];
|
||||
default?: any;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface TagConfig {
|
||||
description: string;
|
||||
required_attrs: string[];
|
||||
optional_attrs?: Record<string, TagAttributeConfig>;
|
||||
requires_children?: boolean;
|
||||
child_tag?: string;
|
||||
child_required_attrs?: string[];
|
||||
category?: string; // e.g., "labeling" or "layout" for controls; "image", "text", etc. for objects
|
||||
}
|
||||
|
||||
export interface LabelStudioTagConfig {
|
||||
objects: Record<string, TagConfig>;
|
||||
controls: Record<string, TagConfig>;
|
||||
}
|
||||
|
||||
/**
|
||||
* UI-friendly representation of a tag for selection
|
||||
*/
|
||||
export interface TagOption {
|
||||
value: string;
|
||||
label: string;
|
||||
description: string;
|
||||
category: "object" | "control";
|
||||
requiresChildren: boolean;
|
||||
childTag?: string;
|
||||
requiredAttrs: string[];
|
||||
optionalAttrs?: Record<string, TagAttributeConfig>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert backend tag config to frontend tag options
|
||||
* @param config - The full tag configuration from backend
|
||||
* @param includeLabelingOnly - If true, only include controls with category="labeling" (default: true)
|
||||
*/
|
||||
export function parseTagConfig(
|
||||
config: LabelStudioTagConfig,
|
||||
includeLabelingOnly: boolean = true
|
||||
): {
|
||||
objectOptions: TagOption[];
|
||||
controlOptions: TagOption[];
|
||||
} {
|
||||
const objectOptions: TagOption[] = Object.entries(config.objects).map(
|
||||
([key, value]) => ({
|
||||
value: key,
|
||||
label: key,
|
||||
description: value.description,
|
||||
category: "object" as const,
|
||||
requiresChildren: value.requires_children || false,
|
||||
childTag: value.child_tag,
|
||||
requiredAttrs: value.required_attrs,
|
||||
optionalAttrs: value.optional_attrs,
|
||||
})
|
||||
);
|
||||
|
||||
const controlOptions: TagOption[] = Object.entries(config.controls)
|
||||
.filter(([_, value]) => {
|
||||
// If includeLabelingOnly is true, filter out layout controls
|
||||
if (includeLabelingOnly) {
|
||||
return value.category === "labeling";
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.map(([key, value]) => ({
|
||||
value: key,
|
||||
label: key,
|
||||
description: value.description,
|
||||
category: "control" as const,
|
||||
requiresChildren: value.requires_children || false,
|
||||
childTag: value.child_tag,
|
||||
requiredAttrs: value.required_attrs,
|
||||
optionalAttrs: value.optional_attrs,
|
||||
}));
|
||||
|
||||
return { objectOptions, controlOptions };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user-friendly display name for control types
|
||||
*/
|
||||
export function getControlDisplayName(controlType: string): string {
|
||||
const displayNames: Record<string, string> = {
|
||||
Choices: "选项 (单选/多选)",
|
||||
RectangleLabels: "矩形框",
|
||||
PolygonLabels: "多边形",
|
||||
Labels: "标签",
|
||||
TextArea: "文本区域",
|
||||
Rating: "评分",
|
||||
Taxonomy: "分类树",
|
||||
Ranker: "排序",
|
||||
List: "列表",
|
||||
BrushLabels: "画笔分割",
|
||||
EllipseLabels: "椭圆",
|
||||
KeyPointLabels: "关键点",
|
||||
Rectangle: "矩形",
|
||||
Polygon: "多边形",
|
||||
Ellipse: "椭圆",
|
||||
KeyPoint: "关键点",
|
||||
Brush: "画笔",
|
||||
Number: "数字输入",
|
||||
DateTime: "日期时间",
|
||||
Relation: "关系",
|
||||
Relations: "关系组",
|
||||
Pairwise: "成对比较",
|
||||
};
|
||||
|
||||
return displayNames[controlType] || controlType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user-friendly display name for object types
|
||||
*/
|
||||
export function getObjectDisplayName(objectType: string): string {
|
||||
const displayNames: Record<string, string> = {
|
||||
Image: "图像",
|
||||
Text: "文本",
|
||||
Audio: "音频",
|
||||
Video: "视频",
|
||||
HyperText: "HTML内容",
|
||||
PDF: "PDF文档",
|
||||
Markdown: "Markdown内容",
|
||||
Paragraphs: "段落",
|
||||
Table: "表格",
|
||||
AudioPlus: "高级音频",
|
||||
Timeseries: "时间序列",
|
||||
Vector: "向量数据",
|
||||
Chat: "对话数据",
|
||||
};
|
||||
|
||||
return displayNames[objectType] || objectType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Group control types by common usage patterns
|
||||
*/
|
||||
export function getControlGroups(): Record<
|
||||
string,
|
||||
{ label: string; controls: string[] }
|
||||
> {
|
||||
return {
|
||||
classification: {
|
||||
label: "分类标注",
|
||||
controls: ["Choices", "Taxonomy", "Labels", "Rating"],
|
||||
},
|
||||
detection: {
|
||||
label: "目标检测",
|
||||
controls: [
|
||||
"RectangleLabels",
|
||||
"PolygonLabels",
|
||||
"EllipseLabels",
|
||||
"KeyPointLabels",
|
||||
"Rectangle",
|
||||
"Polygon",
|
||||
"Ellipse",
|
||||
"KeyPoint",
|
||||
],
|
||||
},
|
||||
segmentation: {
|
||||
label: "分割标注",
|
||||
controls: ["BrushLabels", "Brush", "BitmaskLabels", "MagicWand"],
|
||||
},
|
||||
text: {
|
||||
label: "文本输入",
|
||||
controls: ["TextArea", "Number", "DateTime"],
|
||||
},
|
||||
other: {
|
||||
label: "其他",
|
||||
controls: [
|
||||
"TimeseriesLabels",
|
||||
"VectorLabels",
|
||||
"ParagraphLabels",
|
||||
"VideoRectangle",
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,133 +1,133 @@
|
||||
import { useState } from "react";
|
||||
import { Steps, Button, message, Form } from "antd";
|
||||
import { SaveOutlined } from "@ant-design/icons";
|
||||
import { Link, useNavigate } from "react-router";
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
import { createCleaningTaskUsingPost } from "../cleansing.api";
|
||||
import CreateTaskStepOne from "./components/CreateTaskStepOne";
|
||||
import { useCreateStepTwo } from "./hooks/useCreateStepTwo";
|
||||
import { DatasetType } from "@/pages/DataManagement/dataset.model";
|
||||
|
||||
export default function CleansingTaskCreate() {
|
||||
const navigate = useNavigate();
|
||||
const [form] = Form.useForm();
|
||||
const [taskConfig, setTaskConfig] = useState({
|
||||
name: "",
|
||||
description: "",
|
||||
srcDatasetId: "",
|
||||
srcDatasetName: "",
|
||||
destDatasetName: "",
|
||||
destDatasetType: DatasetType.TEXT,
|
||||
type: DatasetType.TEXT,
|
||||
});
|
||||
|
||||
const {
|
||||
renderStepTwo,
|
||||
selectedOperators,
|
||||
currentStep,
|
||||
handlePrev,
|
||||
handleNext,
|
||||
} = useCreateStepTwo();
|
||||
|
||||
const handleSave = async () => {
|
||||
const task = {
|
||||
...taskConfig,
|
||||
instance: selectedOperators.map((item) => ({
|
||||
id: item.id,
|
||||
overrides: {
|
||||
...item.defaultParams,
|
||||
...item.overrides,
|
||||
},
|
||||
inputs: item.inputs,
|
||||
outputs: item.outputs,
|
||||
})),
|
||||
};
|
||||
navigate("/data/cleansing?view=task");
|
||||
await createCleaningTaskUsingPost(task);
|
||||
message.success("任务已创建");
|
||||
};
|
||||
|
||||
const canProceed = () => {
|
||||
switch (currentStep) {
|
||||
case 1: {
|
||||
const values = form.getFieldsValue();
|
||||
return (
|
||||
values.name &&
|
||||
values.srcDatasetId &&
|
||||
values.destDatasetName &&
|
||||
values.destDatasetType
|
||||
);
|
||||
}
|
||||
case 2:
|
||||
return selectedOperators.length > 0;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const renderStepContent = () => {
|
||||
switch (currentStep) {
|
||||
case 1:
|
||||
return (
|
||||
<CreateTaskStepOne
|
||||
form={form}
|
||||
taskConfig={taskConfig}
|
||||
setTaskConfig={setTaskConfig}
|
||||
/>
|
||||
);
|
||||
case 2:
|
||||
return renderStepTwo;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center">
|
||||
<Link to="/data/cleansing">
|
||||
<Button type="text">
|
||||
<ArrowLeft className="w-4 h-4 mr-1" />
|
||||
</Button>
|
||||
</Link>
|
||||
<h1 className="text-xl font-bold">创建清洗任务</h1>
|
||||
</div>
|
||||
<div className="w-1/2">
|
||||
<Steps
|
||||
size="small"
|
||||
current={currentStep - 1}
|
||||
items={[{ title: "基本信息" }, { title: "算子编排" }]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/* Step Content */}
|
||||
<div className="flex-overflow-auto bg-white border-card">
|
||||
<div className="flex-1 overflow-auto m-6">{renderStepContent()}</div>
|
||||
<div className="flex justify-end p-6 gap-3 border-top">
|
||||
<Button onClick={() => navigate("/data/cleansing")}>取消</Button>
|
||||
{currentStep > 1 && <Button onClick={handlePrev}>上一步</Button>}
|
||||
{currentStep === 2 ? (
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<SaveOutlined />}
|
||||
onClick={handleSave}
|
||||
disabled={!canProceed()}
|
||||
>
|
||||
创建任务
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={handleNext}
|
||||
disabled={!canProceed()}
|
||||
>
|
||||
下一步
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
import { useState } from "react";
|
||||
import { Steps, Button, message, Form } from "antd";
|
||||
import { SaveOutlined } from "@ant-design/icons";
|
||||
import { Link, useNavigate } from "react-router";
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
import { createCleaningTaskUsingPost } from "../cleansing.api";
|
||||
import CreateTaskStepOne from "./components/CreateTaskStepOne";
|
||||
import { useCreateStepTwo } from "./hooks/useCreateStepTwo";
|
||||
import { DatasetType } from "@/pages/DataManagement/dataset.model";
|
||||
|
||||
export default function CleansingTaskCreate() {
|
||||
const navigate = useNavigate();
|
||||
const [form] = Form.useForm();
|
||||
const [taskConfig, setTaskConfig] = useState({
|
||||
name: "",
|
||||
description: "",
|
||||
srcDatasetId: "",
|
||||
srcDatasetName: "",
|
||||
destDatasetName: "",
|
||||
destDatasetType: DatasetType.TEXT,
|
||||
type: DatasetType.TEXT,
|
||||
});
|
||||
|
||||
const {
|
||||
renderStepTwo,
|
||||
selectedOperators,
|
||||
currentStep,
|
||||
handlePrev,
|
||||
handleNext,
|
||||
} = useCreateStepTwo();
|
||||
|
||||
const handleSave = async () => {
|
||||
const task = {
|
||||
...taskConfig,
|
||||
instance: selectedOperators.map((item) => ({
|
||||
id: item.id,
|
||||
overrides: {
|
||||
...item.defaultParams,
|
||||
...item.overrides,
|
||||
},
|
||||
inputs: item.inputs,
|
||||
outputs: item.outputs,
|
||||
})),
|
||||
};
|
||||
navigate("/data/cleansing?view=task");
|
||||
await createCleaningTaskUsingPost(task);
|
||||
message.success("任务已创建");
|
||||
};
|
||||
|
||||
const canProceed = () => {
|
||||
switch (currentStep) {
|
||||
case 1: {
|
||||
const values = form.getFieldsValue();
|
||||
return (
|
||||
values.name &&
|
||||
values.srcDatasetId &&
|
||||
values.destDatasetName &&
|
||||
values.destDatasetType
|
||||
);
|
||||
}
|
||||
case 2:
|
||||
return selectedOperators.length > 0;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const renderStepContent = () => {
|
||||
switch (currentStep) {
|
||||
case 1:
|
||||
return (
|
||||
<CreateTaskStepOne
|
||||
form={form}
|
||||
taskConfig={taskConfig}
|
||||
setTaskConfig={setTaskConfig}
|
||||
/>
|
||||
);
|
||||
case 2:
|
||||
return renderStepTwo;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center">
|
||||
<Link to="/data/cleansing">
|
||||
<Button type="text">
|
||||
<ArrowLeft className="w-4 h-4 mr-1" />
|
||||
</Button>
|
||||
</Link>
|
||||
<h1 className="text-xl font-bold">创建清洗任务</h1>
|
||||
</div>
|
||||
<div className="w-1/2">
|
||||
<Steps
|
||||
size="small"
|
||||
current={currentStep - 1}
|
||||
items={[{ title: "基本信息" }, { title: "算子编排" }]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/* Step Content */}
|
||||
<div className="flex-overflow-auto bg-white border-card">
|
||||
<div className="flex-1 overflow-auto m-6">{renderStepContent()}</div>
|
||||
<div className="flex justify-end p-6 gap-3 border-top">
|
||||
<Button onClick={() => navigate("/data/cleansing")}>取消</Button>
|
||||
{currentStep > 1 && <Button onClick={handlePrev}>上一步</Button>}
|
||||
{currentStep === 2 ? (
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<SaveOutlined />}
|
||||
onClick={handleSave}
|
||||
disabled={!canProceed()}
|
||||
>
|
||||
创建任务
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={handleNext}
|
||||
disabled={!canProceed()}
|
||||
>
|
||||
下一步
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,142 +1,142 @@
|
||||
import {useEffect, useState} from "react";
|
||||
import {Button, Steps, Form, message} from "antd";
|
||||
import {Link, useNavigate, useParams} from "react-router";
|
||||
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
import {
|
||||
createCleaningTemplateUsingPost,
|
||||
queryCleaningTemplateByIdUsingGet,
|
||||
updateCleaningTemplateByIdUsingPut
|
||||
} from "../cleansing.api";
|
||||
import CleansingTemplateStepOne from "./components/CreateTemplateStepOne";
|
||||
import { useCreateStepTwo } from "./hooks/useCreateStepTwo";
|
||||
|
||||
export default function CleansingTemplateCreate() {
|
||||
const { id = "" } = useParams()
|
||||
const navigate = useNavigate();
|
||||
const [form] = Form.useForm();
|
||||
const [templateConfig, setTemplateConfig] = useState({
|
||||
name: "",
|
||||
description: "",
|
||||
});
|
||||
|
||||
const fetchTemplateDetail = async () => {
|
||||
if (!id) return;
|
||||
try {
|
||||
const { data } = await queryCleaningTemplateByIdUsingGet(id);
|
||||
setTemplateConfig(data);
|
||||
} catch (error) {
|
||||
message.error("获取任务详情失败");
|
||||
navigate("/data/cleansing");
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchTemplateDetail()
|
||||
}, [id]);
|
||||
|
||||
const handleSave = async () => {
|
||||
const template = {
|
||||
...templateConfig,
|
||||
instance: selectedOperators.map((item) => ({
|
||||
id: item.id,
|
||||
overrides: {
|
||||
...item.defaultParams,
|
||||
...item.overrides,
|
||||
},
|
||||
inputs: item.inputs,
|
||||
outputs: item.outputs,
|
||||
})),
|
||||
};
|
||||
|
||||
!id && await createCleaningTemplateUsingPost(template) && message.success("模板创建成功");
|
||||
id && await updateCleaningTemplateByIdUsingPut(id, template) && message.success("模板更新成功");
|
||||
navigate("/data/cleansing?view=template");
|
||||
};
|
||||
|
||||
const {
|
||||
renderStepTwo,
|
||||
selectedOperators,
|
||||
currentStep,
|
||||
handlePrev,
|
||||
handleNext,
|
||||
} = useCreateStepTwo();
|
||||
|
||||
const canProceed = () => {
|
||||
const values = form.getFieldsValue();
|
||||
|
||||
switch (currentStep) {
|
||||
case 1:
|
||||
return values.name;
|
||||
case 2:
|
||||
return selectedOperators.length > 0;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const renderStepContent = () => {
|
||||
switch (currentStep) {
|
||||
case 1:
|
||||
return (
|
||||
<CleansingTemplateStepOne
|
||||
form={form}
|
||||
templateConfig={templateConfig}
|
||||
setTemplateConfig={setTemplateConfig}
|
||||
/>
|
||||
);
|
||||
case 2:
|
||||
return renderStepTwo;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center">
|
||||
<Link to="/data/cleansing">
|
||||
<Button type="text">
|
||||
<ArrowLeft className="w-4 h-4 mr-1" />
|
||||
</Button>
|
||||
</Link>
|
||||
<h1 className="text-xl font-bold">{id ? '更新清洗模板' : '创建清洗模板'}</h1>
|
||||
</div>
|
||||
<div className="w-1/2">
|
||||
<Steps
|
||||
size="small"
|
||||
current={currentStep}
|
||||
items={[{ title: "基本信息" }, { title: "算子编排" }]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-overflow-auto border-card">
|
||||
<div className="flex-1 overflow-auto m-6">{renderStepContent()}</div>
|
||||
<div className="flex justify-end p-6 gap-3 border-top">
|
||||
<Button onClick={() => navigate("/data/cleansing")}>取消</Button>
|
||||
{currentStep > 1 && <Button onClick={handlePrev}>上一步</Button>}
|
||||
{currentStep === 2 ? (
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={handleSave}
|
||||
disabled={!canProceed()}
|
||||
>
|
||||
{id ? '更新模板' : '创建模板'}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={handleNext}
|
||||
disabled={!canProceed()}
|
||||
>
|
||||
下一步
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
import {useEffect, useState} from "react";
|
||||
import {Button, Steps, Form, message} from "antd";
|
||||
import {Link, useNavigate, useParams} from "react-router";
|
||||
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
import {
|
||||
createCleaningTemplateUsingPost,
|
||||
queryCleaningTemplateByIdUsingGet,
|
||||
updateCleaningTemplateByIdUsingPut
|
||||
} from "../cleansing.api";
|
||||
import CleansingTemplateStepOne from "./components/CreateTemplateStepOne";
|
||||
import { useCreateStepTwo } from "./hooks/useCreateStepTwo";
|
||||
|
||||
export default function CleansingTemplateCreate() {
|
||||
const { id = "" } = useParams()
|
||||
const navigate = useNavigate();
|
||||
const [form] = Form.useForm();
|
||||
const [templateConfig, setTemplateConfig] = useState({
|
||||
name: "",
|
||||
description: "",
|
||||
});
|
||||
|
||||
const fetchTemplateDetail = async () => {
|
||||
if (!id) return;
|
||||
try {
|
||||
const { data } = await queryCleaningTemplateByIdUsingGet(id);
|
||||
setTemplateConfig(data);
|
||||
} catch (error) {
|
||||
message.error("获取任务详情失败");
|
||||
navigate("/data/cleansing");
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchTemplateDetail()
|
||||
}, [id]);
|
||||
|
||||
const handleSave = async () => {
|
||||
const template = {
|
||||
...templateConfig,
|
||||
instance: selectedOperators.map((item) => ({
|
||||
id: item.id,
|
||||
overrides: {
|
||||
...item.defaultParams,
|
||||
...item.overrides,
|
||||
},
|
||||
inputs: item.inputs,
|
||||
outputs: item.outputs,
|
||||
})),
|
||||
};
|
||||
|
||||
!id && await createCleaningTemplateUsingPost(template) && message.success("模板创建成功");
|
||||
id && await updateCleaningTemplateByIdUsingPut(id, template) && message.success("模板更新成功");
|
||||
navigate("/data/cleansing?view=template");
|
||||
};
|
||||
|
||||
const {
|
||||
renderStepTwo,
|
||||
selectedOperators,
|
||||
currentStep,
|
||||
handlePrev,
|
||||
handleNext,
|
||||
} = useCreateStepTwo();
|
||||
|
||||
const canProceed = () => {
|
||||
const values = form.getFieldsValue();
|
||||
|
||||
switch (currentStep) {
|
||||
case 1:
|
||||
return values.name;
|
||||
case 2:
|
||||
return selectedOperators.length > 0;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const renderStepContent = () => {
|
||||
switch (currentStep) {
|
||||
case 1:
|
||||
return (
|
||||
<CleansingTemplateStepOne
|
||||
form={form}
|
||||
templateConfig={templateConfig}
|
||||
setTemplateConfig={setTemplateConfig}
|
||||
/>
|
||||
);
|
||||
case 2:
|
||||
return renderStepTwo;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center">
|
||||
<Link to="/data/cleansing">
|
||||
<Button type="text">
|
||||
<ArrowLeft className="w-4 h-4 mr-1" />
|
||||
</Button>
|
||||
</Link>
|
||||
<h1 className="text-xl font-bold">{id ? '更新清洗模板' : '创建清洗模板'}</h1>
|
||||
</div>
|
||||
<div className="w-1/2">
|
||||
<Steps
|
||||
size="small"
|
||||
current={currentStep}
|
||||
items={[{ title: "基本信息" }, { title: "算子编排" }]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-overflow-auto border-card">
|
||||
<div className="flex-1 overflow-auto m-6">{renderStepContent()}</div>
|
||||
<div className="flex justify-end p-6 gap-3 border-top">
|
||||
<Button onClick={() => navigate("/data/cleansing")}>取消</Button>
|
||||
{currentStep > 1 && <Button onClick={handlePrev}>上一步</Button>}
|
||||
{currentStep === 2 ? (
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={handleSave}
|
||||
disabled={!canProceed()}
|
||||
>
|
||||
{id ? '更新模板' : '创建模板'}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={handleNext}
|
||||
disabled={!canProceed()}
|
||||
>
|
||||
下一步
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,113 +1,113 @@
|
||||
import RadioCard from "@/components/RadioCard";
|
||||
import { queryDatasetsUsingGet } from "@/pages/DataManagement/dataset.api";
|
||||
import { datasetTypes, mapDataset } from "@/pages/DataManagement/dataset.const";
|
||||
import {
|
||||
Dataset,
|
||||
DatasetSubType,
|
||||
DatasetType,
|
||||
} from "@/pages/DataManagement/dataset.model";
|
||||
import { Input, Select, Form } from "antd";
|
||||
import TextArea from "antd/es/input/TextArea";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export default function CreateTaskStepOne({
|
||||
form,
|
||||
taskConfig,
|
||||
setTaskConfig,
|
||||
}: {
|
||||
form: any;
|
||||
taskConfig: {
|
||||
name: string;
|
||||
description: string;
|
||||
datasetId: string;
|
||||
destDatasetName: string;
|
||||
type: DatasetType;
|
||||
destDatasetType: DatasetSubType;
|
||||
};
|
||||
setTaskConfig: (config: any) => void;
|
||||
}) {
|
||||
const [datasets, setDatasets] = useState<Dataset[]>([]);
|
||||
|
||||
const fetchDatasets = async () => {
|
||||
const { data } = await queryDatasetsUsingGet({ page: 1, size: 1000 });
|
||||
setDatasets(data.content.map(mapDataset) || []);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchDatasets();
|
||||
}, []);
|
||||
|
||||
const handleValuesChange = (currentValue, allValues) => {
|
||||
const [key, value] = Object.entries(currentValue)[0];
|
||||
let dataset = null;
|
||||
if (key === "srcDatasetId") {
|
||||
dataset = datasets.find((d) => d.id === value);
|
||||
setTaskConfig({
|
||||
...taskConfig,
|
||||
...allValues,
|
||||
srcDatasetName: dataset?.name || "",
|
||||
});
|
||||
} else {
|
||||
setTaskConfig({ ...taskConfig, ...allValues });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Form
|
||||
layout="vertical"
|
||||
form={form}
|
||||
initialValues={taskConfig}
|
||||
onValuesChange={handleValuesChange}
|
||||
>
|
||||
<h2 className="font-medium text-gray-900 text-base mb-2">任务信息</h2>
|
||||
<Form.Item label="名称" name="name" required>
|
||||
<Input placeholder="输入清洗任务名称" />
|
||||
</Form.Item>
|
||||
<Form.Item label="描述" name="description">
|
||||
<TextArea placeholder="描述清洗任务的目标和要求" rows={4} />
|
||||
</Form.Item>
|
||||
<h2 className="font-medium text-gray-900 pt-6 mb-2 text-base">
|
||||
数据源选择
|
||||
</h2>
|
||||
<Form.Item label="源数据集" name="srcDatasetId" required>
|
||||
<Select
|
||||
placeholder="请选择源数据集"
|
||||
options={datasets.map((dataset) => {
|
||||
return {
|
||||
label: (
|
||||
<div className="flex items-center justify-between gap-3 py-2">
|
||||
<div className="flex items-center font-sm text-gray-900">
|
||||
<span className="mr-2">{dataset.icon}</span>
|
||||
<span>{dataset.name}</span>
|
||||
</div>
|
||||
<div className="text-xs text-gray-500">{dataset.size}</div>
|
||||
</div>
|
||||
),
|
||||
value: dataset.id,
|
||||
};
|
||||
})}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="目标数据集名称" name="destDatasetName" required>
|
||||
<Input placeholder="输入目标数据集名称" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="目标数据集类型"
|
||||
name="destDatasetType"
|
||||
rules={[{ required: true, message: "请选择目标数据集类型" }]}
|
||||
>
|
||||
<RadioCard
|
||||
options={datasetTypes}
|
||||
value={taskConfig.destDatasetType}
|
||||
onChange={(type) => {
|
||||
form.setFieldValue("destDatasetType", type);
|
||||
setTaskConfig({
|
||||
...taskConfig,
|
||||
destDatasetType: type as DatasetSubType,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
import RadioCard from "@/components/RadioCard";
|
||||
import { queryDatasetsUsingGet } from "@/pages/DataManagement/dataset.api";
|
||||
import { datasetTypes, mapDataset } from "@/pages/DataManagement/dataset.const";
|
||||
import {
|
||||
Dataset,
|
||||
DatasetSubType,
|
||||
DatasetType,
|
||||
} from "@/pages/DataManagement/dataset.model";
|
||||
import { Input, Select, Form } from "antd";
|
||||
import TextArea from "antd/es/input/TextArea";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export default function CreateTaskStepOne({
|
||||
form,
|
||||
taskConfig,
|
||||
setTaskConfig,
|
||||
}: {
|
||||
form: any;
|
||||
taskConfig: {
|
||||
name: string;
|
||||
description: string;
|
||||
datasetId: string;
|
||||
destDatasetName: string;
|
||||
type: DatasetType;
|
||||
destDatasetType: DatasetSubType;
|
||||
};
|
||||
setTaskConfig: (config: any) => void;
|
||||
}) {
|
||||
const [datasets, setDatasets] = useState<Dataset[]>([]);
|
||||
|
||||
const fetchDatasets = async () => {
|
||||
const { data } = await queryDatasetsUsingGet({ page: 1, size: 1000 });
|
||||
setDatasets(data.content.map(mapDataset) || []);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchDatasets();
|
||||
}, []);
|
||||
|
||||
const handleValuesChange = (currentValue, allValues) => {
|
||||
const [key, value] = Object.entries(currentValue)[0];
|
||||
let dataset = null;
|
||||
if (key === "srcDatasetId") {
|
||||
dataset = datasets.find((d) => d.id === value);
|
||||
setTaskConfig({
|
||||
...taskConfig,
|
||||
...allValues,
|
||||
srcDatasetName: dataset?.name || "",
|
||||
});
|
||||
} else {
|
||||
setTaskConfig({ ...taskConfig, ...allValues });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Form
|
||||
layout="vertical"
|
||||
form={form}
|
||||
initialValues={taskConfig}
|
||||
onValuesChange={handleValuesChange}
|
||||
>
|
||||
<h2 className="font-medium text-gray-900 text-base mb-2">任务信息</h2>
|
||||
<Form.Item label="名称" name="name" required>
|
||||
<Input placeholder="输入清洗任务名称" />
|
||||
</Form.Item>
|
||||
<Form.Item label="描述" name="description">
|
||||
<TextArea placeholder="描述清洗任务的目标和要求" rows={4} />
|
||||
</Form.Item>
|
||||
<h2 className="font-medium text-gray-900 pt-6 mb-2 text-base">
|
||||
数据源选择
|
||||
</h2>
|
||||
<Form.Item label="源数据集" name="srcDatasetId" required>
|
||||
<Select
|
||||
placeholder="请选择源数据集"
|
||||
options={datasets.map((dataset) => {
|
||||
return {
|
||||
label: (
|
||||
<div className="flex items-center justify-between gap-3 py-2">
|
||||
<div className="flex items-center font-sm text-gray-900">
|
||||
<span className="mr-2">{dataset.icon}</span>
|
||||
<span>{dataset.name}</span>
|
||||
</div>
|
||||
<div className="text-xs text-gray-500">{dataset.size}</div>
|
||||
</div>
|
||||
),
|
||||
value: dataset.id,
|
||||
};
|
||||
})}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="目标数据集名称" name="destDatasetName" required>
|
||||
<Input placeholder="输入目标数据集名称" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="目标数据集类型"
|
||||
name="destDatasetType"
|
||||
rules={[{ required: true, message: "请选择目标数据集类型" }]}
|
||||
>
|
||||
<RadioCard
|
||||
options={datasetTypes}
|
||||
value={taskConfig.destDatasetType}
|
||||
onChange={(type) => {
|
||||
form.setFieldValue("destDatasetType", type);
|
||||
setTaskConfig({
|
||||
...taskConfig,
|
||||
destDatasetType: type as DatasetSubType,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,44 +1,44 @@
|
||||
import { Input, Form } from "antd";
|
||||
import {useEffect} from "react";
|
||||
|
||||
const { TextArea } = Input;
|
||||
|
||||
export default function CreateTemplateStepOne({
|
||||
form,
|
||||
templateConfig,
|
||||
setTemplateConfig,
|
||||
}: {
|
||||
form: any;
|
||||
templateConfig: { name: string; description: string; type: string };
|
||||
setTemplateConfig: React.Dispatch<
|
||||
React.SetStateAction<{ name: string; description: string; type: string }>
|
||||
>;
|
||||
}) {
|
||||
const handleValuesChange = (_, allValues) => {
|
||||
setTemplateConfig({ ...templateConfig, ...allValues });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
form.setFieldsValue(templateConfig);
|
||||
}, [templateConfig]);
|
||||
|
||||
return (
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
initialValues={templateConfig}
|
||||
onValuesChange={handleValuesChange}
|
||||
>
|
||||
<Form.Item
|
||||
label="模板名称"
|
||||
name="name"
|
||||
rules={[{ required: true, message: "请输入模板名称" }]}
|
||||
>
|
||||
<Input placeholder="输入模板名称" />
|
||||
</Form.Item>
|
||||
<Form.Item label="模板描述" name="description">
|
||||
<TextArea placeholder="描述模板的用途和特点" rows={4} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
import { Input, Form } from "antd";
|
||||
import {useEffect} from "react";
|
||||
|
||||
const { TextArea } = Input;
|
||||
|
||||
export default function CreateTemplateStepOne({
|
||||
form,
|
||||
templateConfig,
|
||||
setTemplateConfig,
|
||||
}: {
|
||||
form: any;
|
||||
templateConfig: { name: string; description: string; type: string };
|
||||
setTemplateConfig: React.Dispatch<
|
||||
React.SetStateAction<{ name: string; description: string; type: string }>
|
||||
>;
|
||||
}) {
|
||||
const handleValuesChange = (_, allValues) => {
|
||||
setTemplateConfig({ ...templateConfig, ...allValues });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
form.setFieldsValue(templateConfig);
|
||||
}, [templateConfig]);
|
||||
|
||||
return (
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
initialValues={templateConfig}
|
||||
onValuesChange={handleValuesChange}
|
||||
>
|
||||
<Form.Item
|
||||
label="模板名称"
|
||||
name="name"
|
||||
rules={[{ required: true, message: "请输入模板名称" }]}
|
||||
>
|
||||
<Input placeholder="输入模板名称" />
|
||||
</Form.Item>
|
||||
<Form.Item label="模板描述" name="description">
|
||||
<TextArea placeholder="描述模板的用途和特点" rows={4} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,81 +1,81 @@
|
||||
import React from "react";
|
||||
import { Tag, Divider, Form } from "antd";
|
||||
import ParamConfig from "./ParamConfig";
|
||||
import { Settings } from "lucide-react";
|
||||
import { OperatorI } from "@/pages/OperatorMarket/operator.model";
|
||||
|
||||
// OperatorConfig/OperatorTemplate 类型需根据主文件实际导入
|
||||
interface OperatorConfigProps {
|
||||
selectedOp: OperatorI;
|
||||
renderParamConfig?: (
|
||||
operator: OperatorI,
|
||||
paramKey: string,
|
||||
param: any
|
||||
) => React.ReactNode;
|
||||
handleConfigChange?: (
|
||||
operatorId: string,
|
||||
paramKey: string,
|
||||
value: any
|
||||
) => void;
|
||||
}
|
||||
|
||||
const OperatorConfig: React.FC<OperatorConfigProps> = ({
|
||||
selectedOp,
|
||||
renderParamConfig,
|
||||
handleConfigChange,
|
||||
}) => {
|
||||
return (
|
||||
<div className="w-1/4 min-w-3xs flex flex-col h-full">
|
||||
<div className="px-4 pb-4 border-b border-gray-200">
|
||||
<span className="font-semibold text-base flex items-center gap-2">
|
||||
<Settings />
|
||||
参数配置
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex-1 overflow-auto p-4">
|
||||
{selectedOp ? (
|
||||
<div>
|
||||
<div className="mb-4">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="font-medium">{selectedOp.name}</span>
|
||||
</div>
|
||||
<div className="text-sm text-gray-500">
|
||||
{selectedOp.description}
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-1 mt-2">
|
||||
{selectedOp?.tags?.map((tag: string) => (
|
||||
<Tag key={tag} color="default">
|
||||
{tag}
|
||||
</Tag>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<Divider />
|
||||
<Form layout="vertical">
|
||||
{Object.entries(selectedOp.configs).map(([key, param]) =>
|
||||
renderParamConfig ? (
|
||||
renderParamConfig(selectedOp, key, param)
|
||||
) : (
|
||||
<ParamConfig
|
||||
key={key}
|
||||
operator={selectedOp}
|
||||
paramKey={key}
|
||||
param={param}
|
||||
onParamChange={handleConfigChange}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</Form>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-12 text-gray-400">
|
||||
<Settings className="w-full w-10 h-10 mb-4 opacity-50" />
|
||||
<div>请选择一个算子进行参数配置</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default OperatorConfig;
|
||||
import React from "react";
|
||||
import { Tag, Divider, Form } from "antd";
|
||||
import ParamConfig from "./ParamConfig";
|
||||
import { Settings } from "lucide-react";
|
||||
import { OperatorI } from "@/pages/OperatorMarket/operator.model";
|
||||
|
||||
// OperatorConfig/OperatorTemplate 类型需根据主文件实际导入
|
||||
interface OperatorConfigProps {
|
||||
selectedOp: OperatorI;
|
||||
renderParamConfig?: (
|
||||
operator: OperatorI,
|
||||
paramKey: string,
|
||||
param: any
|
||||
) => React.ReactNode;
|
||||
handleConfigChange?: (
|
||||
operatorId: string,
|
||||
paramKey: string,
|
||||
value: any
|
||||
) => void;
|
||||
}
|
||||
|
||||
const OperatorConfig: React.FC<OperatorConfigProps> = ({
|
||||
selectedOp,
|
||||
renderParamConfig,
|
||||
handleConfigChange,
|
||||
}) => {
|
||||
return (
|
||||
<div className="w-1/4 min-w-3xs flex flex-col h-full">
|
||||
<div className="px-4 pb-4 border-b border-gray-200">
|
||||
<span className="font-semibold text-base flex items-center gap-2">
|
||||
<Settings />
|
||||
参数配置
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex-1 overflow-auto p-4">
|
||||
{selectedOp ? (
|
||||
<div>
|
||||
<div className="mb-4">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="font-medium">{selectedOp.name}</span>
|
||||
</div>
|
||||
<div className="text-sm text-gray-500">
|
||||
{selectedOp.description}
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-1 mt-2">
|
||||
{selectedOp?.tags?.map((tag: string) => (
|
||||
<Tag key={tag} color="default">
|
||||
{tag}
|
||||
</Tag>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<Divider />
|
||||
<Form layout="vertical">
|
||||
{Object.entries(selectedOp.configs).map(([key, param]) =>
|
||||
renderParamConfig ? (
|
||||
renderParamConfig(selectedOp, key, param)
|
||||
) : (
|
||||
<ParamConfig
|
||||
key={key}
|
||||
operator={selectedOp}
|
||||
paramKey={key}
|
||||
param={param}
|
||||
onParamChange={handleConfigChange}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</Form>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-12 text-gray-400">
|
||||
<Settings className="w-full w-10 h-10 mb-4 opacity-50" />
|
||||
<div>请选择一个算子进行参数配置</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default OperatorConfig;
|
||||
|
||||
@@ -1,287 +1,287 @@
|
||||
import React, {useEffect, useMemo, useState} from "react";
|
||||
import {Button, Card, Checkbox, Collapse, Input, Select, Tag, Tooltip,} from "antd";
|
||||
import {SearchOutlined, StarFilled, StarOutlined} from "@ant-design/icons";
|
||||
import {CategoryI, OperatorI} from "@/pages/OperatorMarket/operator.model";
|
||||
import {Layers} from "lucide-react";
|
||||
import {updateOperatorByIdUsingPut} from "@/pages/OperatorMarket/operator.api.ts";
|
||||
|
||||
interface OperatorListProps {
|
||||
operators: OperatorI[];
|
||||
favorites: Set<string>;
|
||||
showPoppular?: boolean;
|
||||
toggleFavorite: (id: string) => void;
|
||||
toggleOperator: (operator: OperatorI) => void;
|
||||
selectedOperators: OperatorI[];
|
||||
onDragOperator: (
|
||||
e: React.DragEvent,
|
||||
item: OperatorI,
|
||||
source: "library"
|
||||
) => void;
|
||||
}
|
||||
|
||||
const handleStar = async (operator: OperatorI, toggleFavorite: (id: string) => void) => {
|
||||
const data = {
|
||||
id: operator.id,
|
||||
isStar: !operator.isStar
|
||||
};
|
||||
await updateOperatorByIdUsingPut(operator.id, data);
|
||||
toggleFavorite(operator.id)
|
||||
}
|
||||
|
||||
const OperatorList: React.FC<OperatorListProps> = ({
|
||||
operators,
|
||||
favorites,
|
||||
toggleFavorite,
|
||||
toggleOperator,
|
||||
selectedOperators,
|
||||
onDragOperator,
|
||||
}) => (
|
||||
<div className="grid grid-cols-1 gap-2">
|
||||
{operators.map((operator) => {
|
||||
// 判断是否已选
|
||||
const isSelected = selectedOperators.some((op) => op.id === operator.id);
|
||||
return (
|
||||
<Card
|
||||
size="small"
|
||||
key={operator.id}
|
||||
draggable
|
||||
hoverable
|
||||
onDragStart={(e) => onDragOperator(e, operator, "library")}
|
||||
onClick={() => toggleOperator(operator)}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex flex-1 min-w-0 items-center gap-2">
|
||||
<Checkbox checked={isSelected} />
|
||||
<span className="flex-1 min-w-0 font-medium text-sm overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
{operator.name}
|
||||
</span>
|
||||
</div>
|
||||
<span
|
||||
className="cursor-pointer"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
handleStar(operator, toggleFavorite);
|
||||
}}
|
||||
>
|
||||
{favorites.has(operator.id) ? (
|
||||
<StarFilled style={{ color: "#FFD700" }} />
|
||||
) : (
|
||||
<StarOutlined />
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
|
||||
interface OperatorLibraryProps {
|
||||
selectedOperators: OperatorI[];
|
||||
operatorList: OperatorI[];
|
||||
categoryOptions: CategoryI[];
|
||||
setSelectedOperators: (operators: OperatorI[]) => void;
|
||||
toggleOperator: (template: OperatorI) => void;
|
||||
handleDragStart: (
|
||||
e: React.DragEvent,
|
||||
item: OperatorI,
|
||||
source: "library"
|
||||
) => void;
|
||||
}
|
||||
|
||||
const OperatorLibrary: React.FC<OperatorLibraryProps> = ({
|
||||
selectedOperators,
|
||||
operatorList,
|
||||
categoryOptions,
|
||||
setSelectedOperators,
|
||||
toggleOperator,
|
||||
handleDragStart,
|
||||
}) => {
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [showFavorites, setShowFavorites] = useState(false);
|
||||
const [favorites, setFavorites] = useState<Set<string>>(new Set());
|
||||
const [selectedCategory, setSelectedCategory] = useState<string>("all");
|
||||
const [expandedCategories, setExpandedCategories] = useState<Set<string>>(
|
||||
new Set([])
|
||||
);
|
||||
|
||||
// 按分类分组
|
||||
const groupedOperators = useMemo(() => {
|
||||
const groups: { [key: string]: OperatorI[] } = {};
|
||||
categoryOptions.forEach((cat: any) => {
|
||||
groups[cat.name] = {
|
||||
...cat,
|
||||
operators: operatorList.filter((op) => op.categories?.includes(cat.id)),
|
||||
};
|
||||
});
|
||||
|
||||
if (selectedCategory && selectedCategory !== "all") {
|
||||
Object.keys(groups).forEach((key) => {
|
||||
if (groups[key].id !== selectedCategory) {
|
||||
delete groups[key];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (searchTerm) {
|
||||
Object.keys(groups).forEach((key) => {
|
||||
groups[key].operators = groups[key].operators.filter((operator) =>
|
||||
operator.name.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
if (groups[key].operators.length === 0) {
|
||||
delete groups[key];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (showFavorites) {
|
||||
Object.keys(groups).forEach((key) => {
|
||||
groups[key].operators = groups[key].operators.filter((operator) =>
|
||||
favorites.has(operator.id)
|
||||
);
|
||||
if (groups[key].operators.length === 0) {
|
||||
delete groups[key];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setExpandedCategories(new Set(Object.keys(groups)));
|
||||
return groups;
|
||||
}, [categoryOptions, selectedCategory, searchTerm, showFavorites]);
|
||||
|
||||
// 过滤算子
|
||||
const filteredOperators = useMemo(() => {
|
||||
return Object.values(groupedOperators).flatMap(
|
||||
(category) => category.operators
|
||||
);
|
||||
}, [groupedOperators]);
|
||||
|
||||
// 收藏切换
|
||||
const toggleFavorite = (operatorId: string) => {
|
||||
const newFavorites = new Set(favorites);
|
||||
if (newFavorites.has(operatorId)) {
|
||||
newFavorites.delete(operatorId);
|
||||
} else {
|
||||
newFavorites.add(operatorId);
|
||||
}
|
||||
setFavorites(newFavorites);
|
||||
};
|
||||
|
||||
const fetchFavorite = async () => {
|
||||
const newFavorites = new Set(favorites);
|
||||
operatorList.forEach(item => {
|
||||
item.isStar && newFavorites.add(item.id);
|
||||
});
|
||||
setFavorites(newFavorites);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchFavorite()
|
||||
}, [operatorList]);
|
||||
|
||||
// 全选分类算子
|
||||
const handleSelectAll = (operators: OperatorI[]) => {
|
||||
const newSelected = [...selectedOperators];
|
||||
operators.forEach((operator) => {
|
||||
if (!newSelected.some((op) => op.id === operator.id)) {
|
||||
newSelected.push(operator);
|
||||
}
|
||||
});
|
||||
setSelectedOperators(newSelected);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-1/4 h-full min-w-3xs flex flex-col">
|
||||
<div className="pb-4 border-b border-gray-200">
|
||||
<span className="flex items-center font-semibold text-base">
|
||||
<Layers className="w-4 h-4 mr-2" />
|
||||
算子库({filteredOperators.length})
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-col h-full pt-4 pr-4 overflow-hidden">
|
||||
{/* 过滤器 */}
|
||||
<div className="flex flex-wrap gap-2 border-b border-gray-100 pb-4">
|
||||
<Input
|
||||
prefix={<SearchOutlined />}
|
||||
placeholder="搜索算子名称..."
|
||||
value={searchTerm}
|
||||
allowClear
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
/>
|
||||
<Select
|
||||
value={selectedCategory}
|
||||
options={[{ label: "全部分类", value: "all" }, ...categoryOptions]}
|
||||
onChange={setSelectedCategory}
|
||||
className="flex-1"
|
||||
placeholder="选择分类"
|
||||
></Select>
|
||||
<Tooltip title="只看收藏">
|
||||
<span
|
||||
className="cursor-pointer"
|
||||
onClick={() => setShowFavorites(!showFavorites)}
|
||||
>
|
||||
{showFavorites ? (
|
||||
<StarFilled style={{ color: "#FFD700" }} />
|
||||
) : (
|
||||
<StarOutlined />
|
||||
)}
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
{/* 算子列表 */}
|
||||
<div className="flex-1 overflow-auto">
|
||||
{/* 分类算子 */}
|
||||
<Collapse
|
||||
ghost
|
||||
activeKey={Array.from(expandedCategories)}
|
||||
onChange={(keys) =>
|
||||
setExpandedCategories(
|
||||
new Set(Array.isArray(keys) ? keys : [keys])
|
||||
)
|
||||
}
|
||||
>
|
||||
{Object.entries(groupedOperators).map(([key, category]) => (
|
||||
<Collapse.Panel
|
||||
key={key}
|
||||
header={
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<span className="flex items-center gap-2">
|
||||
<span>{category.name}</span>
|
||||
<Tag>{category.operators.length}</Tag>
|
||||
</span>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleSelectAll(category.operators);
|
||||
}}
|
||||
>
|
||||
全选
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<OperatorList
|
||||
selectedOperators={selectedOperators}
|
||||
operators={category.operators}
|
||||
favorites={favorites}
|
||||
toggleOperator={toggleOperator}
|
||||
onDragOperator={handleDragStart}
|
||||
toggleFavorite={toggleFavorite}
|
||||
/>
|
||||
</Collapse.Panel>
|
||||
))}
|
||||
</Collapse>
|
||||
{filteredOperators.length === 0 && (
|
||||
<div className="text-center py-8 text-gray-400">
|
||||
<SearchOutlined className="text-3xl mb-2 opacity-50" />
|
||||
<div>未找到匹配的算子</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default OperatorLibrary;
|
||||
import React, {useEffect, useMemo, useState} from "react";
|
||||
import {Button, Card, Checkbox, Collapse, Input, Select, Tag, Tooltip,} from "antd";
|
||||
import {SearchOutlined, StarFilled, StarOutlined} from "@ant-design/icons";
|
||||
import {CategoryI, OperatorI} from "@/pages/OperatorMarket/operator.model";
|
||||
import {Layers} from "lucide-react";
|
||||
import {updateOperatorByIdUsingPut} from "@/pages/OperatorMarket/operator.api.ts";
|
||||
|
||||
interface OperatorListProps {
|
||||
operators: OperatorI[];
|
||||
favorites: Set<string>;
|
||||
showPoppular?: boolean;
|
||||
toggleFavorite: (id: string) => void;
|
||||
toggleOperator: (operator: OperatorI) => void;
|
||||
selectedOperators: OperatorI[];
|
||||
onDragOperator: (
|
||||
e: React.DragEvent,
|
||||
item: OperatorI,
|
||||
source: "library"
|
||||
) => void;
|
||||
}
|
||||
|
||||
const handleStar = async (operator: OperatorI, toggleFavorite: (id: string) => void) => {
|
||||
const data = {
|
||||
id: operator.id,
|
||||
isStar: !operator.isStar
|
||||
};
|
||||
await updateOperatorByIdUsingPut(operator.id, data);
|
||||
toggleFavorite(operator.id)
|
||||
}
|
||||
|
||||
const OperatorList: React.FC<OperatorListProps> = ({
|
||||
operators,
|
||||
favorites,
|
||||
toggleFavorite,
|
||||
toggleOperator,
|
||||
selectedOperators,
|
||||
onDragOperator,
|
||||
}) => (
|
||||
<div className="grid grid-cols-1 gap-2">
|
||||
{operators.map((operator) => {
|
||||
// 判断是否已选
|
||||
const isSelected = selectedOperators.some((op) => op.id === operator.id);
|
||||
return (
|
||||
<Card
|
||||
size="small"
|
||||
key={operator.id}
|
||||
draggable
|
||||
hoverable
|
||||
onDragStart={(e) => onDragOperator(e, operator, "library")}
|
||||
onClick={() => toggleOperator(operator)}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex flex-1 min-w-0 items-center gap-2">
|
||||
<Checkbox checked={isSelected} />
|
||||
<span className="flex-1 min-w-0 font-medium text-sm overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
{operator.name}
|
||||
</span>
|
||||
</div>
|
||||
<span
|
||||
className="cursor-pointer"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
handleStar(operator, toggleFavorite);
|
||||
}}
|
||||
>
|
||||
{favorites.has(operator.id) ? (
|
||||
<StarFilled style={{ color: "#FFD700" }} />
|
||||
) : (
|
||||
<StarOutlined />
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
|
||||
interface OperatorLibraryProps {
|
||||
selectedOperators: OperatorI[];
|
||||
operatorList: OperatorI[];
|
||||
categoryOptions: CategoryI[];
|
||||
setSelectedOperators: (operators: OperatorI[]) => void;
|
||||
toggleOperator: (template: OperatorI) => void;
|
||||
handleDragStart: (
|
||||
e: React.DragEvent,
|
||||
item: OperatorI,
|
||||
source: "library"
|
||||
) => void;
|
||||
}
|
||||
|
||||
const OperatorLibrary: React.FC<OperatorLibraryProps> = ({
|
||||
selectedOperators,
|
||||
operatorList,
|
||||
categoryOptions,
|
||||
setSelectedOperators,
|
||||
toggleOperator,
|
||||
handleDragStart,
|
||||
}) => {
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [showFavorites, setShowFavorites] = useState(false);
|
||||
const [favorites, setFavorites] = useState<Set<string>>(new Set());
|
||||
const [selectedCategory, setSelectedCategory] = useState<string>("all");
|
||||
const [expandedCategories, setExpandedCategories] = useState<Set<string>>(
|
||||
new Set([])
|
||||
);
|
||||
|
||||
// 按分类分组
|
||||
const groupedOperators = useMemo(() => {
|
||||
const groups: { [key: string]: OperatorI[] } = {};
|
||||
categoryOptions.forEach((cat: any) => {
|
||||
groups[cat.name] = {
|
||||
...cat,
|
||||
operators: operatorList.filter((op) => op.categories?.includes(cat.id)),
|
||||
};
|
||||
});
|
||||
|
||||
if (selectedCategory && selectedCategory !== "all") {
|
||||
Object.keys(groups).forEach((key) => {
|
||||
if (groups[key].id !== selectedCategory) {
|
||||
delete groups[key];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (searchTerm) {
|
||||
Object.keys(groups).forEach((key) => {
|
||||
groups[key].operators = groups[key].operators.filter((operator) =>
|
||||
operator.name.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
if (groups[key].operators.length === 0) {
|
||||
delete groups[key];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (showFavorites) {
|
||||
Object.keys(groups).forEach((key) => {
|
||||
groups[key].operators = groups[key].operators.filter((operator) =>
|
||||
favorites.has(operator.id)
|
||||
);
|
||||
if (groups[key].operators.length === 0) {
|
||||
delete groups[key];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setExpandedCategories(new Set(Object.keys(groups)));
|
||||
return groups;
|
||||
}, [categoryOptions, selectedCategory, searchTerm, showFavorites]);
|
||||
|
||||
// 过滤算子
|
||||
const filteredOperators = useMemo(() => {
|
||||
return Object.values(groupedOperators).flatMap(
|
||||
(category) => category.operators
|
||||
);
|
||||
}, [groupedOperators]);
|
||||
|
||||
// 收藏切换
|
||||
const toggleFavorite = (operatorId: string) => {
|
||||
const newFavorites = new Set(favorites);
|
||||
if (newFavorites.has(operatorId)) {
|
||||
newFavorites.delete(operatorId);
|
||||
} else {
|
||||
newFavorites.add(operatorId);
|
||||
}
|
||||
setFavorites(newFavorites);
|
||||
};
|
||||
|
||||
const fetchFavorite = async () => {
|
||||
const newFavorites = new Set(favorites);
|
||||
operatorList.forEach(item => {
|
||||
item.isStar && newFavorites.add(item.id);
|
||||
});
|
||||
setFavorites(newFavorites);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchFavorite()
|
||||
}, [operatorList]);
|
||||
|
||||
// 全选分类算子
|
||||
const handleSelectAll = (operators: OperatorI[]) => {
|
||||
const newSelected = [...selectedOperators];
|
||||
operators.forEach((operator) => {
|
||||
if (!newSelected.some((op) => op.id === operator.id)) {
|
||||
newSelected.push(operator);
|
||||
}
|
||||
});
|
||||
setSelectedOperators(newSelected);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-1/4 h-full min-w-3xs flex flex-col">
|
||||
<div className="pb-4 border-b border-gray-200">
|
||||
<span className="flex items-center font-semibold text-base">
|
||||
<Layers className="w-4 h-4 mr-2" />
|
||||
算子库({filteredOperators.length})
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-col h-full pt-4 pr-4 overflow-hidden">
|
||||
{/* 过滤器 */}
|
||||
<div className="flex flex-wrap gap-2 border-b border-gray-100 pb-4">
|
||||
<Input
|
||||
prefix={<SearchOutlined />}
|
||||
placeholder="搜索算子名称..."
|
||||
value={searchTerm}
|
||||
allowClear
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
/>
|
||||
<Select
|
||||
value={selectedCategory}
|
||||
options={[{ label: "全部分类", value: "all" }, ...categoryOptions]}
|
||||
onChange={setSelectedCategory}
|
||||
className="flex-1"
|
||||
placeholder="选择分类"
|
||||
></Select>
|
||||
<Tooltip title="只看收藏">
|
||||
<span
|
||||
className="cursor-pointer"
|
||||
onClick={() => setShowFavorites(!showFavorites)}
|
||||
>
|
||||
{showFavorites ? (
|
||||
<StarFilled style={{ color: "#FFD700" }} />
|
||||
) : (
|
||||
<StarOutlined />
|
||||
)}
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
{/* 算子列表 */}
|
||||
<div className="flex-1 overflow-auto">
|
||||
{/* 分类算子 */}
|
||||
<Collapse
|
||||
ghost
|
||||
activeKey={Array.from(expandedCategories)}
|
||||
onChange={(keys) =>
|
||||
setExpandedCategories(
|
||||
new Set(Array.isArray(keys) ? keys : [keys])
|
||||
)
|
||||
}
|
||||
>
|
||||
{Object.entries(groupedOperators).map(([key, category]) => (
|
||||
<Collapse.Panel
|
||||
key={key}
|
||||
header={
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<span className="flex items-center gap-2">
|
||||
<span>{category.name}</span>
|
||||
<Tag>{category.operators.length}</Tag>
|
||||
</span>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleSelectAll(category.operators);
|
||||
}}
|
||||
>
|
||||
全选
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<OperatorList
|
||||
selectedOperators={selectedOperators}
|
||||
operators={category.operators}
|
||||
favorites={favorites}
|
||||
toggleOperator={toggleOperator}
|
||||
onDragOperator={handleDragStart}
|
||||
toggleFavorite={toggleFavorite}
|
||||
/>
|
||||
</Collapse.Panel>
|
||||
))}
|
||||
</Collapse>
|
||||
{filteredOperators.length === 0 && (
|
||||
<div className="text-center py-8 text-gray-400">
|
||||
<SearchOutlined className="text-3xl mb-2 opacity-50" />
|
||||
<div>未找到匹配的算子</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default OperatorLibrary;
|
||||
|
||||
@@ -1,213 +1,213 @@
|
||||
import React, {useMemo, useState} from "react";
|
||||
import { Card, Input, Tag, Select, Button } from "antd";
|
||||
import { DeleteOutlined } from "@ant-design/icons";
|
||||
import { CleansingTemplate } from "../../cleansing.model";
|
||||
import { Workflow } from "lucide-react";
|
||||
import {CategoryI, OperatorI} from "@/pages/OperatorMarket/operator.model";
|
||||
|
||||
interface OperatorFlowProps {
|
||||
selectedOperators: OperatorI[];
|
||||
configOperator: OperatorI | null;
|
||||
templates: CleansingTemplate[];
|
||||
currentTemplate: CleansingTemplate | null;
|
||||
categoryOptions: [];
|
||||
setCurrentTemplate: (template: CleansingTemplate | null) => void;
|
||||
removeOperator: (id: string) => void;
|
||||
setSelectedOperators: (operators: OperatorI[]) => void;
|
||||
setConfigOperator: (operator: OperatorI | null) => void;
|
||||
handleDragStart: (
|
||||
e: React.DragEvent,
|
||||
operator: OperatorI,
|
||||
source: "sort"
|
||||
) => void;
|
||||
handleItemDragOver: (e: React.DragEvent, itemId: string) => void;
|
||||
handleItemDragLeave: (e: React.DragEvent) => void;
|
||||
handleItemDrop: (e: React.DragEvent, index: number) => void;
|
||||
handleContainerDragOver: (e: React.DragEvent) => void;
|
||||
handleContainerDragLeave: (e: React.DragEvent) => void;
|
||||
handleDragEnd: (e: React.DragEvent) => void;
|
||||
handleDropToContainer: (e: React.DragEvent) => void;
|
||||
}
|
||||
|
||||
const OperatorFlow: React.FC<OperatorFlowProps> = ({
|
||||
selectedOperators,
|
||||
configOperator,
|
||||
templates,
|
||||
currentTemplate,
|
||||
categoryOptions,
|
||||
setSelectedOperators,
|
||||
setConfigOperator,
|
||||
removeOperator,
|
||||
setCurrentTemplate,
|
||||
handleDragStart,
|
||||
handleItemDragLeave,
|
||||
handleItemDragOver,
|
||||
handleItemDrop,
|
||||
handleContainerDragLeave,
|
||||
handleDropToContainer,
|
||||
handleDragEnd,
|
||||
}) => {
|
||||
const [editingIndex, setEditingIndex] = useState<string | null>(null);
|
||||
|
||||
const categoryMap = useMemo(() => {
|
||||
const map: { [key: string]: CategoryI } = {};
|
||||
categoryOptions.forEach((cat: any) => {
|
||||
map[cat.id] = {
|
||||
...cat,
|
||||
};
|
||||
});
|
||||
return map;
|
||||
}, [categoryOptions]);
|
||||
|
||||
// 添加编号修改处理函数
|
||||
const handleIndexChange = (operatorId: string, newIndex: string) => {
|
||||
const index = Number.parseInt(newIndex);
|
||||
if (isNaN(index) || index < 1 || index > selectedOperators.length) {
|
||||
return; // 无效输入,不处理
|
||||
}
|
||||
|
||||
const currentIndex = selectedOperators.findIndex(
|
||||
(op) => op.id === operatorId
|
||||
);
|
||||
if (currentIndex === -1) return;
|
||||
|
||||
const targetIndex = index - 1; // 转换为0基索引
|
||||
if (currentIndex === targetIndex) return; // 位置没有变化
|
||||
|
||||
const newOperators = [...selectedOperators];
|
||||
const [movedOperator] = newOperators.splice(currentIndex, 1);
|
||||
newOperators.splice(targetIndex, 0, movedOperator);
|
||||
|
||||
setSelectedOperators(newOperators);
|
||||
setEditingIndex(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-1/2 h-full min-w-xs flex-1 flex flex-col border-x border-gray-200">
|
||||
{/* 工具栏 */}
|
||||
<div className="px-4 pb-2 border-b border-gray-200">
|
||||
<div className="flex flex-wrap gap-2 justify-between items-start">
|
||||
<span className="font-semibold text-base flex items-center gap-2">
|
||||
<Workflow className="w-5 h-5" />
|
||||
算子编排({selectedOperators.length}){" "}
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
onClick={() => {
|
||||
setConfigOperator(null);
|
||||
setSelectedOperators([]);
|
||||
}}
|
||||
disabled={selectedOperators.length === 0}
|
||||
>
|
||||
清空
|
||||
</Button>
|
||||
</span>
|
||||
<Select
|
||||
placeholder="选择模板"
|
||||
className="min-w-64"
|
||||
options={templates}
|
||||
value={currentTemplate?.value}
|
||||
onChange={(value) =>
|
||||
setCurrentTemplate(
|
||||
templates.find((t) => t.value === value) || null
|
||||
)
|
||||
}
|
||||
></Select>
|
||||
</div>
|
||||
</div>
|
||||
{/* 编排区域 */}
|
||||
<div
|
||||
className="flex-overflow-auto p-4 gap-2"
|
||||
onDragOver={(e) => e.preventDefault()}
|
||||
onDragLeave={handleContainerDragLeave}
|
||||
onDrop={handleDropToContainer}
|
||||
>
|
||||
{selectedOperators.map((operator, index) => (
|
||||
<Card
|
||||
size="small"
|
||||
key={operator.id}
|
||||
style={
|
||||
configOperator?.id === operator.id
|
||||
? { borderColor: "#1677ff" }
|
||||
: {}
|
||||
}
|
||||
hoverable
|
||||
draggable
|
||||
onDragStart={(e) => handleDragStart(e, operator, "sort")}
|
||||
onDragEnd={handleDragEnd}
|
||||
onDragOver={(e) => handleItemDragOver(e, operator.id)}
|
||||
onDragLeave={handleItemDragLeave}
|
||||
onDrop={(e) => handleItemDrop(e, index)}
|
||||
onClick={() => setConfigOperator(operator)}
|
||||
>
|
||||
<div className="flex items-center gap-1">
|
||||
{/* 可编辑编号 */}
|
||||
<span>⋮⋮</span>
|
||||
{editingIndex === operator.id ? (
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
max={selectedOperators.length}
|
||||
defaultValue={index + 1}
|
||||
className="w-10 h-6 text-xs text-center"
|
||||
style={{ width: 60 }}
|
||||
autoFocus
|
||||
onBlur={(e) => handleIndexChange(operator.id, e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter")
|
||||
handleIndexChange(
|
||||
operator.id,
|
||||
(e.target as HTMLInputElement).value
|
||||
);
|
||||
else if (e.key === "Escape") setEditingIndex(null);
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
) : (
|
||||
<Tag
|
||||
color="default"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setEditingIndex(operator.id);
|
||||
}}
|
||||
>
|
||||
{index + 1}
|
||||
</Tag>
|
||||
)}
|
||||
{/* 算子图标和名称 */}
|
||||
<div className="flex items-center gap-2 min-w-0 flex-1">
|
||||
<span className="font-medium text-sm truncate">
|
||||
{operator.name}
|
||||
</span>
|
||||
</div>
|
||||
{operator?.categories?.map((categoryId) => {
|
||||
return <Tag color="default">{categoryMap[categoryId].name}</Tag>
|
||||
})}
|
||||
{/* 操作按钮 */}
|
||||
<span
|
||||
className="cursor-pointer text-red-500"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
removeOperator(operator.id);
|
||||
}}
|
||||
>
|
||||
<DeleteOutlined />
|
||||
</span>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
{selectedOperators.length === 0 && (
|
||||
<div className="text-center py-16 text-gray-400 border-2 border-dashed border-gray-100 rounded-lg">
|
||||
<Workflow className="w-full h-10 mb-4 opacity-50" />
|
||||
<div className="text-lg font-medium mb-2">开始构建您的算子流程</div>
|
||||
<div className="text-sm">
|
||||
从左侧算子库拖拽算子到此处,或点击算子添加
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default OperatorFlow;
|
||||
import React, {useMemo, useState} from "react";
|
||||
import { Card, Input, Tag, Select, Button } from "antd";
|
||||
import { DeleteOutlined } from "@ant-design/icons";
|
||||
import { CleansingTemplate } from "../../cleansing.model";
|
||||
import { Workflow } from "lucide-react";
|
||||
import {CategoryI, OperatorI} from "@/pages/OperatorMarket/operator.model";
|
||||
|
||||
interface OperatorFlowProps {
|
||||
selectedOperators: OperatorI[];
|
||||
configOperator: OperatorI | null;
|
||||
templates: CleansingTemplate[];
|
||||
currentTemplate: CleansingTemplate | null;
|
||||
categoryOptions: [];
|
||||
setCurrentTemplate: (template: CleansingTemplate | null) => void;
|
||||
removeOperator: (id: string) => void;
|
||||
setSelectedOperators: (operators: OperatorI[]) => void;
|
||||
setConfigOperator: (operator: OperatorI | null) => void;
|
||||
handleDragStart: (
|
||||
e: React.DragEvent,
|
||||
operator: OperatorI,
|
||||
source: "sort"
|
||||
) => void;
|
||||
handleItemDragOver: (e: React.DragEvent, itemId: string) => void;
|
||||
handleItemDragLeave: (e: React.DragEvent) => void;
|
||||
handleItemDrop: (e: React.DragEvent, index: number) => void;
|
||||
handleContainerDragOver: (e: React.DragEvent) => void;
|
||||
handleContainerDragLeave: (e: React.DragEvent) => void;
|
||||
handleDragEnd: (e: React.DragEvent) => void;
|
||||
handleDropToContainer: (e: React.DragEvent) => void;
|
||||
}
|
||||
|
||||
const OperatorFlow: React.FC<OperatorFlowProps> = ({
|
||||
selectedOperators,
|
||||
configOperator,
|
||||
templates,
|
||||
currentTemplate,
|
||||
categoryOptions,
|
||||
setSelectedOperators,
|
||||
setConfigOperator,
|
||||
removeOperator,
|
||||
setCurrentTemplate,
|
||||
handleDragStart,
|
||||
handleItemDragLeave,
|
||||
handleItemDragOver,
|
||||
handleItemDrop,
|
||||
handleContainerDragLeave,
|
||||
handleDropToContainer,
|
||||
handleDragEnd,
|
||||
}) => {
|
||||
const [editingIndex, setEditingIndex] = useState<string | null>(null);
|
||||
|
||||
const categoryMap = useMemo(() => {
|
||||
const map: { [key: string]: CategoryI } = {};
|
||||
categoryOptions.forEach((cat: any) => {
|
||||
map[cat.id] = {
|
||||
...cat,
|
||||
};
|
||||
});
|
||||
return map;
|
||||
}, [categoryOptions]);
|
||||
|
||||
// 添加编号修改处理函数
|
||||
const handleIndexChange = (operatorId: string, newIndex: string) => {
|
||||
const index = Number.parseInt(newIndex);
|
||||
if (isNaN(index) || index < 1 || index > selectedOperators.length) {
|
||||
return; // 无效输入,不处理
|
||||
}
|
||||
|
||||
const currentIndex = selectedOperators.findIndex(
|
||||
(op) => op.id === operatorId
|
||||
);
|
||||
if (currentIndex === -1) return;
|
||||
|
||||
const targetIndex = index - 1; // 转换为0基索引
|
||||
if (currentIndex === targetIndex) return; // 位置没有变化
|
||||
|
||||
const newOperators = [...selectedOperators];
|
||||
const [movedOperator] = newOperators.splice(currentIndex, 1);
|
||||
newOperators.splice(targetIndex, 0, movedOperator);
|
||||
|
||||
setSelectedOperators(newOperators);
|
||||
setEditingIndex(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-1/2 h-full min-w-xs flex-1 flex flex-col border-x border-gray-200">
|
||||
{/* 工具栏 */}
|
||||
<div className="px-4 pb-2 border-b border-gray-200">
|
||||
<div className="flex flex-wrap gap-2 justify-between items-start">
|
||||
<span className="font-semibold text-base flex items-center gap-2">
|
||||
<Workflow className="w-5 h-5" />
|
||||
算子编排({selectedOperators.length}){" "}
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
onClick={() => {
|
||||
setConfigOperator(null);
|
||||
setSelectedOperators([]);
|
||||
}}
|
||||
disabled={selectedOperators.length === 0}
|
||||
>
|
||||
清空
|
||||
</Button>
|
||||
</span>
|
||||
<Select
|
||||
placeholder="选择模板"
|
||||
className="min-w-64"
|
||||
options={templates}
|
||||
value={currentTemplate?.value}
|
||||
onChange={(value) =>
|
||||
setCurrentTemplate(
|
||||
templates.find((t) => t.value === value) || null
|
||||
)
|
||||
}
|
||||
></Select>
|
||||
</div>
|
||||
</div>
|
||||
{/* 编排区域 */}
|
||||
<div
|
||||
className="flex-overflow-auto p-4 gap-2"
|
||||
onDragOver={(e) => e.preventDefault()}
|
||||
onDragLeave={handleContainerDragLeave}
|
||||
onDrop={handleDropToContainer}
|
||||
>
|
||||
{selectedOperators.map((operator, index) => (
|
||||
<Card
|
||||
size="small"
|
||||
key={operator.id}
|
||||
style={
|
||||
configOperator?.id === operator.id
|
||||
? { borderColor: "#1677ff" }
|
||||
: {}
|
||||
}
|
||||
hoverable
|
||||
draggable
|
||||
onDragStart={(e) => handleDragStart(e, operator, "sort")}
|
||||
onDragEnd={handleDragEnd}
|
||||
onDragOver={(e) => handleItemDragOver(e, operator.id)}
|
||||
onDragLeave={handleItemDragLeave}
|
||||
onDrop={(e) => handleItemDrop(e, index)}
|
||||
onClick={() => setConfigOperator(operator)}
|
||||
>
|
||||
<div className="flex items-center gap-1">
|
||||
{/* 可编辑编号 */}
|
||||
<span>⋮⋮</span>
|
||||
{editingIndex === operator.id ? (
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
max={selectedOperators.length}
|
||||
defaultValue={index + 1}
|
||||
className="w-10 h-6 text-xs text-center"
|
||||
style={{ width: 60 }}
|
||||
autoFocus
|
||||
onBlur={(e) => handleIndexChange(operator.id, e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter")
|
||||
handleIndexChange(
|
||||
operator.id,
|
||||
(e.target as HTMLInputElement).value
|
||||
);
|
||||
else if (e.key === "Escape") setEditingIndex(null);
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
) : (
|
||||
<Tag
|
||||
color="default"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setEditingIndex(operator.id);
|
||||
}}
|
||||
>
|
||||
{index + 1}
|
||||
</Tag>
|
||||
)}
|
||||
{/* 算子图标和名称 */}
|
||||
<div className="flex items-center gap-2 min-w-0 flex-1">
|
||||
<span className="font-medium text-sm truncate">
|
||||
{operator.name}
|
||||
</span>
|
||||
</div>
|
||||
{operator?.categories?.map((categoryId) => {
|
||||
return <Tag color="default">{categoryMap[categoryId].name}</Tag>
|
||||
})}
|
||||
{/* 操作按钮 */}
|
||||
<span
|
||||
className="cursor-pointer text-red-500"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
removeOperator(operator.id);
|
||||
}}
|
||||
>
|
||||
<DeleteOutlined />
|
||||
</span>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
{selectedOperators.length === 0 && (
|
||||
<div className="text-center py-16 text-gray-400 border-2 border-dashed border-gray-100 rounded-lg">
|
||||
<Workflow className="w-full h-10 mb-4 opacity-50" />
|
||||
<div className="text-lg font-medium mb-2">开始构建您的算子流程</div>
|
||||
<div className="text-sm">
|
||||
从左侧算子库拖拽算子到此处,或点击算子添加
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default OperatorFlow;
|
||||
|
||||
@@ -1,245 +1,245 @@
|
||||
import React from "react";
|
||||
import {
|
||||
Input,
|
||||
Select,
|
||||
Radio,
|
||||
Checkbox,
|
||||
Form,
|
||||
InputNumber,
|
||||
Slider,
|
||||
Space,
|
||||
} from "antd";
|
||||
import { ConfigI, OperatorI } from "@/pages/OperatorMarket/operator.model";
|
||||
|
||||
interface ParamConfigProps {
|
||||
operator: OperatorI;
|
||||
paramKey: string;
|
||||
param: ConfigI;
|
||||
onParamChange?: (operatorId: string, paramKey: string, value: any) => void;
|
||||
}
|
||||
|
||||
const ParamConfig: React.FC<ParamConfigProps> = ({
|
||||
operator,
|
||||
paramKey,
|
||||
param,
|
||||
onParamChange,
|
||||
}) => {
|
||||
if (!param) return null;
|
||||
let defaultVal: any = param.defaultVal;
|
||||
if (param.type === "range") {
|
||||
|
||||
defaultVal = Array.isArray(param.defaultVal)
|
||||
? param.defaultVal
|
||||
: [
|
||||
param?.properties?.[0]?.defaultVal,
|
||||
param?.properties?.[1]?.defaultVal,
|
||||
];
|
||||
}
|
||||
const [value, setValue] = React.useState(param.value || defaultVal);
|
||||
const updateValue = (newValue: any) => {
|
||||
setValue(newValue);
|
||||
return onParamChange && onParamChange(operator.id, paramKey, newValue);
|
||||
};
|
||||
|
||||
switch (param.type) {
|
||||
case "input":
|
||||
return (
|
||||
<Form.Item
|
||||
label={param.name}
|
||||
tooltip={param.description}
|
||||
key={paramKey}
|
||||
>
|
||||
<Input
|
||||
value={value}
|
||||
onChange={(e) => updateValue(e.target.value)}
|
||||
placeholder={`请输入${param.name}`}
|
||||
className="w-full"
|
||||
/>
|
||||
</Form.Item>
|
||||
);
|
||||
case "select":
|
||||
return (
|
||||
<Form.Item
|
||||
label={param.name}
|
||||
tooltip={param.description}
|
||||
key={paramKey}
|
||||
>
|
||||
<Select
|
||||
value={value}
|
||||
onChange={updateValue}
|
||||
options={(param.options || []).map((option: any) =>
|
||||
typeof option === "string"
|
||||
? { label: option, value: option }
|
||||
: option
|
||||
)}
|
||||
placeholder={`请选择${param.name}`}
|
||||
className="w-full"
|
||||
/>
|
||||
</Form.Item>
|
||||
);
|
||||
case "radio":
|
||||
return (
|
||||
<Form.Item
|
||||
label={param.name}
|
||||
tooltip={param.description}
|
||||
key={paramKey}
|
||||
>
|
||||
<Radio.Group
|
||||
value={value}
|
||||
onChange={(e) => updateValue(e.target.value)}
|
||||
>
|
||||
{(param.options || []).map((option: any) => (
|
||||
<Radio
|
||||
key={typeof option === "string" ? option : option.value}
|
||||
value={typeof option === "string" ? option : option.value}
|
||||
>
|
||||
{typeof option === "string" ? option : option.label}
|
||||
</Radio>
|
||||
))}
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
);
|
||||
case "checkbox":
|
||||
return (
|
||||
<Form.Item
|
||||
label={param.name}
|
||||
tooltip={param.description}
|
||||
key={paramKey}
|
||||
>
|
||||
<Checkbox.Group
|
||||
value={value}
|
||||
onChange={updateValue}
|
||||
options={param.options || []}
|
||||
/>
|
||||
</Form.Item>
|
||||
);
|
||||
case "slider":
|
||||
return (
|
||||
<Form.Item
|
||||
label={param.name}
|
||||
tooltip={param.description}
|
||||
key={paramKey}
|
||||
>
|
||||
<div className="flex items-center gap-1">
|
||||
<Slider
|
||||
value={value}
|
||||
onChange={updateValue}
|
||||
tooltip={{ open: true }}
|
||||
marks={{
|
||||
[param.min || 0]: `${param.min || 0}`,
|
||||
[param.min + (param.max - param.min) / 2]: `${
|
||||
(param.min + param.max) / 2
|
||||
}`,
|
||||
[param.max || 100]: `${param.max || 100}`,
|
||||
}}
|
||||
min={param.min || 0}
|
||||
max={param.max || 100}
|
||||
step={param.step || 1}
|
||||
className="flex-1"
|
||||
/>
|
||||
<InputNumber
|
||||
min={param.min || 0}
|
||||
max={param.max || 100}
|
||||
step={param.step || 1}
|
||||
value={value}
|
||||
onChange={updateValue}
|
||||
style={{ width: 80 }}
|
||||
/>
|
||||
</div>
|
||||
</Form.Item>
|
||||
);
|
||||
case "range": {
|
||||
const min = param.min || param?.properties?.[0]?.min || 0;
|
||||
const max = param.max || param?.properties?.[0]?.max || 1;
|
||||
const step = param.step || param?.properties?.[0]?.step || 0.1;
|
||||
return (
|
||||
<Form.Item
|
||||
label={param.name}
|
||||
tooltip={param.description}
|
||||
key={paramKey}
|
||||
>
|
||||
<Slider
|
||||
value={Array.isArray(value) ? value : [value, value]}
|
||||
onChange={(val) =>
|
||||
updateValue(Array.isArray(val) ? val : [val, val])
|
||||
}
|
||||
range
|
||||
min={min}
|
||||
max={max }
|
||||
step={step}
|
||||
className="w-full"
|
||||
/>
|
||||
<Space>
|
||||
<InputNumber
|
||||
min={min}
|
||||
max={max}
|
||||
value={value[0]}
|
||||
onChange={(val1) => updateValue([val1, value[1]])}
|
||||
changeOnWheel
|
||||
/>
|
||||
~
|
||||
<InputNumber
|
||||
min={min}
|
||||
max={max}
|
||||
value={value[1]}
|
||||
onChange={(val2) => updateValue([value[0], val2])}
|
||||
changeOnWheel
|
||||
/>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
);
|
||||
}
|
||||
case "inputNumber":
|
||||
return (
|
||||
<Form.Item
|
||||
label={param.name}
|
||||
tooltip={param.description}
|
||||
key={paramKey}
|
||||
>
|
||||
<InputNumber
|
||||
value={value}
|
||||
onChange={(val) => updateValue(val)}
|
||||
placeholder={`请输入${param.name}`}
|
||||
className="w-full"
|
||||
min={param.min}
|
||||
max={param.max}
|
||||
step={param.step || 1}
|
||||
/>
|
||||
</Form.Item>
|
||||
);
|
||||
|
||||
case "switch":
|
||||
return (
|
||||
<Form.Item
|
||||
label={param.name}
|
||||
tooltip={param.description}
|
||||
key={paramKey}
|
||||
>
|
||||
<Checkbox
|
||||
checked={value as boolean}
|
||||
onChange={(e) => updateValue(e.target.checked)}
|
||||
>
|
||||
{param.name}
|
||||
</Checkbox>
|
||||
</Form.Item>
|
||||
);
|
||||
case "multiple":
|
||||
return (
|
||||
<div className="pl-4 border-l border-gray-300">
|
||||
{param.properties.map((subParam) => (
|
||||
<ParamConfig
|
||||
key={subParam.key}
|
||||
operator={operator}
|
||||
paramKey={subParam.key}
|
||||
param={subParam}
|
||||
onParamChange={onParamChange}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export default ParamConfig;
|
||||
import React from "react";
|
||||
import {
|
||||
Input,
|
||||
Select,
|
||||
Radio,
|
||||
Checkbox,
|
||||
Form,
|
||||
InputNumber,
|
||||
Slider,
|
||||
Space,
|
||||
} from "antd";
|
||||
import { ConfigI, OperatorI } from "@/pages/OperatorMarket/operator.model";
|
||||
|
||||
interface ParamConfigProps {
|
||||
operator: OperatorI;
|
||||
paramKey: string;
|
||||
param: ConfigI;
|
||||
onParamChange?: (operatorId: string, paramKey: string, value: any) => void;
|
||||
}
|
||||
|
||||
const ParamConfig: React.FC<ParamConfigProps> = ({
|
||||
operator,
|
||||
paramKey,
|
||||
param,
|
||||
onParamChange,
|
||||
}) => {
|
||||
if (!param) return null;
|
||||
let defaultVal: any = param.defaultVal;
|
||||
if (param.type === "range") {
|
||||
|
||||
defaultVal = Array.isArray(param.defaultVal)
|
||||
? param.defaultVal
|
||||
: [
|
||||
param?.properties?.[0]?.defaultVal,
|
||||
param?.properties?.[1]?.defaultVal,
|
||||
];
|
||||
}
|
||||
const [value, setValue] = React.useState(param.value || defaultVal);
|
||||
const updateValue = (newValue: any) => {
|
||||
setValue(newValue);
|
||||
return onParamChange && onParamChange(operator.id, paramKey, newValue);
|
||||
};
|
||||
|
||||
switch (param.type) {
|
||||
case "input":
|
||||
return (
|
||||
<Form.Item
|
||||
label={param.name}
|
||||
tooltip={param.description}
|
||||
key={paramKey}
|
||||
>
|
||||
<Input
|
||||
value={value}
|
||||
onChange={(e) => updateValue(e.target.value)}
|
||||
placeholder={`请输入${param.name}`}
|
||||
className="w-full"
|
||||
/>
|
||||
</Form.Item>
|
||||
);
|
||||
case "select":
|
||||
return (
|
||||
<Form.Item
|
||||
label={param.name}
|
||||
tooltip={param.description}
|
||||
key={paramKey}
|
||||
>
|
||||
<Select
|
||||
value={value}
|
||||
onChange={updateValue}
|
||||
options={(param.options || []).map((option: any) =>
|
||||
typeof option === "string"
|
||||
? { label: option, value: option }
|
||||
: option
|
||||
)}
|
||||
placeholder={`请选择${param.name}`}
|
||||
className="w-full"
|
||||
/>
|
||||
</Form.Item>
|
||||
);
|
||||
case "radio":
|
||||
return (
|
||||
<Form.Item
|
||||
label={param.name}
|
||||
tooltip={param.description}
|
||||
key={paramKey}
|
||||
>
|
||||
<Radio.Group
|
||||
value={value}
|
||||
onChange={(e) => updateValue(e.target.value)}
|
||||
>
|
||||
{(param.options || []).map((option: any) => (
|
||||
<Radio
|
||||
key={typeof option === "string" ? option : option.value}
|
||||
value={typeof option === "string" ? option : option.value}
|
||||
>
|
||||
{typeof option === "string" ? option : option.label}
|
||||
</Radio>
|
||||
))}
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
);
|
||||
case "checkbox":
|
||||
return (
|
||||
<Form.Item
|
||||
label={param.name}
|
||||
tooltip={param.description}
|
||||
key={paramKey}
|
||||
>
|
||||
<Checkbox.Group
|
||||
value={value}
|
||||
onChange={updateValue}
|
||||
options={param.options || []}
|
||||
/>
|
||||
</Form.Item>
|
||||
);
|
||||
case "slider":
|
||||
return (
|
||||
<Form.Item
|
||||
label={param.name}
|
||||
tooltip={param.description}
|
||||
key={paramKey}
|
||||
>
|
||||
<div className="flex items-center gap-1">
|
||||
<Slider
|
||||
value={value}
|
||||
onChange={updateValue}
|
||||
tooltip={{ open: true }}
|
||||
marks={{
|
||||
[param.min || 0]: `${param.min || 0}`,
|
||||
[param.min + (param.max - param.min) / 2]: `${
|
||||
(param.min + param.max) / 2
|
||||
}`,
|
||||
[param.max || 100]: `${param.max || 100}`,
|
||||
}}
|
||||
min={param.min || 0}
|
||||
max={param.max || 100}
|
||||
step={param.step || 1}
|
||||
className="flex-1"
|
||||
/>
|
||||
<InputNumber
|
||||
min={param.min || 0}
|
||||
max={param.max || 100}
|
||||
step={param.step || 1}
|
||||
value={value}
|
||||
onChange={updateValue}
|
||||
style={{ width: 80 }}
|
||||
/>
|
||||
</div>
|
||||
</Form.Item>
|
||||
);
|
||||
case "range": {
|
||||
const min = param.min || param?.properties?.[0]?.min || 0;
|
||||
const max = param.max || param?.properties?.[0]?.max || 1;
|
||||
const step = param.step || param?.properties?.[0]?.step || 0.1;
|
||||
return (
|
||||
<Form.Item
|
||||
label={param.name}
|
||||
tooltip={param.description}
|
||||
key={paramKey}
|
||||
>
|
||||
<Slider
|
||||
value={Array.isArray(value) ? value : [value, value]}
|
||||
onChange={(val) =>
|
||||
updateValue(Array.isArray(val) ? val : [val, val])
|
||||
}
|
||||
range
|
||||
min={min}
|
||||
max={max }
|
||||
step={step}
|
||||
className="w-full"
|
||||
/>
|
||||
<Space>
|
||||
<InputNumber
|
||||
min={min}
|
||||
max={max}
|
||||
value={value[0]}
|
||||
onChange={(val1) => updateValue([val1, value[1]])}
|
||||
changeOnWheel
|
||||
/>
|
||||
~
|
||||
<InputNumber
|
||||
min={min}
|
||||
max={max}
|
||||
value={value[1]}
|
||||
onChange={(val2) => updateValue([value[0], val2])}
|
||||
changeOnWheel
|
||||
/>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
);
|
||||
}
|
||||
case "inputNumber":
|
||||
return (
|
||||
<Form.Item
|
||||
label={param.name}
|
||||
tooltip={param.description}
|
||||
key={paramKey}
|
||||
>
|
||||
<InputNumber
|
||||
value={value}
|
||||
onChange={(val) => updateValue(val)}
|
||||
placeholder={`请输入${param.name}`}
|
||||
className="w-full"
|
||||
min={param.min}
|
||||
max={param.max}
|
||||
step={param.step || 1}
|
||||
/>
|
||||
</Form.Item>
|
||||
);
|
||||
|
||||
case "switch":
|
||||
return (
|
||||
<Form.Item
|
||||
label={param.name}
|
||||
tooltip={param.description}
|
||||
key={paramKey}
|
||||
>
|
||||
<Checkbox
|
||||
checked={value as boolean}
|
||||
onChange={(e) => updateValue(e.target.checked)}
|
||||
>
|
||||
{param.name}
|
||||
</Checkbox>
|
||||
</Form.Item>
|
||||
);
|
||||
case "multiple":
|
||||
return (
|
||||
<div className="pl-4 border-l border-gray-300">
|
||||
{param.properties.map((subParam) => (
|
||||
<ParamConfig
|
||||
key={subParam.key}
|
||||
operator={operator}
|
||||
paramKey={subParam.key}
|
||||
param={subParam}
|
||||
onParamChange={onParamChange}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export default ParamConfig;
|
||||
|
||||
@@ -1,87 +1,87 @@
|
||||
import { useDragOperators } from "./useDragOperators";
|
||||
import { useOperatorOperations } from "./useOperatorOperations";
|
||||
import OperatorConfig from "../components/OperatorConfig";
|
||||
import OperatorLibrary from "../components/OperatorLibrary";
|
||||
import OperatorOrchestration from "../components/OperatorOrchestration";
|
||||
|
||||
export function useCreateStepTwo() {
|
||||
const {
|
||||
operators,
|
||||
selectedOperators,
|
||||
templates,
|
||||
currentTemplate,
|
||||
configOperator,
|
||||
currentStep,
|
||||
categoryOptions,
|
||||
handlePrev,
|
||||
handleNext,
|
||||
setCurrentTemplate,
|
||||
setConfigOperator,
|
||||
setSelectedOperators,
|
||||
handleConfigChange,
|
||||
toggleOperator,
|
||||
removeOperator,
|
||||
} = useOperatorOperations();
|
||||
|
||||
const {
|
||||
handleDragStart,
|
||||
handleDragEnd,
|
||||
handleContainerDragOver,
|
||||
handleContainerDragLeave,
|
||||
handleItemDragOver,
|
||||
handleItemDragLeave,
|
||||
handleItemDrop,
|
||||
handleDropToContainer,
|
||||
} = useDragOperators({
|
||||
operators: selectedOperators,
|
||||
setOperators: setSelectedOperators,
|
||||
});
|
||||
|
||||
const renderStepTwo = (
|
||||
<div className="flex w-full h-full">
|
||||
{/* 左侧算子库 */}
|
||||
<OperatorLibrary
|
||||
categoryOptions={categoryOptions}
|
||||
selectedOperators={selectedOperators}
|
||||
operatorList={operators}
|
||||
setSelectedOperators={setSelectedOperators}
|
||||
toggleOperator={toggleOperator}
|
||||
handleDragStart={handleDragStart}
|
||||
/>
|
||||
|
||||
{/* 中间算子编排区域 */}
|
||||
<OperatorOrchestration
|
||||
selectedOperators={selectedOperators}
|
||||
configOperator={configOperator}
|
||||
templates={templates}
|
||||
currentTemplate={currentTemplate}
|
||||
categoryOptions={categoryOptions}
|
||||
setSelectedOperators={setSelectedOperators}
|
||||
setConfigOperator={setConfigOperator}
|
||||
setCurrentTemplate={setCurrentTemplate}
|
||||
removeOperator={removeOperator}
|
||||
handleDragStart={handleDragStart}
|
||||
handleContainerDragLeave={handleContainerDragLeave}
|
||||
handleContainerDragOver={handleContainerDragOver}
|
||||
handleItemDragOver={handleItemDragOver}
|
||||
handleItemDragLeave={handleItemDragLeave}
|
||||
handleItemDrop={handleItemDrop}
|
||||
handleDropToContainer={handleDropToContainer}
|
||||
handleDragEnd={handleDragEnd}
|
||||
/>
|
||||
|
||||
{/* 右侧参数配置面板 */}
|
||||
<OperatorConfig
|
||||
selectedOp={configOperator}
|
||||
handleConfigChange={handleConfigChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
return {
|
||||
renderStepTwo,
|
||||
selectedOperators,
|
||||
currentStep,
|
||||
handlePrev,
|
||||
handleNext,
|
||||
};
|
||||
}
|
||||
import { useDragOperators } from "./useDragOperators";
|
||||
import { useOperatorOperations } from "./useOperatorOperations";
|
||||
import OperatorConfig from "../components/OperatorConfig";
|
||||
import OperatorLibrary from "../components/OperatorLibrary";
|
||||
import OperatorOrchestration from "../components/OperatorOrchestration";
|
||||
|
||||
export function useCreateStepTwo() {
|
||||
const {
|
||||
operators,
|
||||
selectedOperators,
|
||||
templates,
|
||||
currentTemplate,
|
||||
configOperator,
|
||||
currentStep,
|
||||
categoryOptions,
|
||||
handlePrev,
|
||||
handleNext,
|
||||
setCurrentTemplate,
|
||||
setConfigOperator,
|
||||
setSelectedOperators,
|
||||
handleConfigChange,
|
||||
toggleOperator,
|
||||
removeOperator,
|
||||
} = useOperatorOperations();
|
||||
|
||||
const {
|
||||
handleDragStart,
|
||||
handleDragEnd,
|
||||
handleContainerDragOver,
|
||||
handleContainerDragLeave,
|
||||
handleItemDragOver,
|
||||
handleItemDragLeave,
|
||||
handleItemDrop,
|
||||
handleDropToContainer,
|
||||
} = useDragOperators({
|
||||
operators: selectedOperators,
|
||||
setOperators: setSelectedOperators,
|
||||
});
|
||||
|
||||
const renderStepTwo = (
|
||||
<div className="flex w-full h-full">
|
||||
{/* 左侧算子库 */}
|
||||
<OperatorLibrary
|
||||
categoryOptions={categoryOptions}
|
||||
selectedOperators={selectedOperators}
|
||||
operatorList={operators}
|
||||
setSelectedOperators={setSelectedOperators}
|
||||
toggleOperator={toggleOperator}
|
||||
handleDragStart={handleDragStart}
|
||||
/>
|
||||
|
||||
{/* 中间算子编排区域 */}
|
||||
<OperatorOrchestration
|
||||
selectedOperators={selectedOperators}
|
||||
configOperator={configOperator}
|
||||
templates={templates}
|
||||
currentTemplate={currentTemplate}
|
||||
categoryOptions={categoryOptions}
|
||||
setSelectedOperators={setSelectedOperators}
|
||||
setConfigOperator={setConfigOperator}
|
||||
setCurrentTemplate={setCurrentTemplate}
|
||||
removeOperator={removeOperator}
|
||||
handleDragStart={handleDragStart}
|
||||
handleContainerDragLeave={handleContainerDragLeave}
|
||||
handleContainerDragOver={handleContainerDragOver}
|
||||
handleItemDragOver={handleItemDragOver}
|
||||
handleItemDragLeave={handleItemDragLeave}
|
||||
handleItemDrop={handleItemDrop}
|
||||
handleDropToContainer={handleDropToContainer}
|
||||
handleDragEnd={handleDragEnd}
|
||||
/>
|
||||
|
||||
{/* 右侧参数配置面板 */}
|
||||
<OperatorConfig
|
||||
selectedOp={configOperator}
|
||||
handleConfigChange={handleConfigChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
return {
|
||||
renderStepTwo,
|
||||
selectedOperators,
|
||||
currentStep,
|
||||
handlePrev,
|
||||
handleNext,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,158 +1,158 @@
|
||||
import { OperatorI } from "@/pages/OperatorMarket/operator.model";
|
||||
import React, { useState } from "react";
|
||||
|
||||
export function useDragOperators({
|
||||
operators,
|
||||
setOperators,
|
||||
}: {
|
||||
operators: OperatorI[];
|
||||
setOperators: (operators: OperatorI[]) => void;
|
||||
}) {
|
||||
const [draggingItem, setDraggingItem] = useState<OperatorI | null>(null);
|
||||
const [draggingSource, setDraggingSource] = useState<
|
||||
"library" | "sort" | null
|
||||
>(null);
|
||||
const [insertPosition, setInsertPosition] = useState<
|
||||
"above" | "below" | null
|
||||
>(null);
|
||||
|
||||
// 处理拖拽开始
|
||||
const handleDragStart = (
|
||||
e: React.DragEvent,
|
||||
item: OperatorI,
|
||||
source: "library" | "sort"
|
||||
) => {
|
||||
setDraggingItem({
|
||||
...item,
|
||||
originalId: item.id,
|
||||
});
|
||||
setDraggingSource(source);
|
||||
e.dataTransfer.effectAllowed = "move";
|
||||
};
|
||||
|
||||
// 处理拖拽结束
|
||||
const handleDragEnd = () => {
|
||||
setDraggingItem(null);
|
||||
setInsertPosition(null);
|
||||
};
|
||||
|
||||
// 处理容器拖拽经过
|
||||
const handleContainerDragOver = (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
// 处理容器拖拽离开
|
||||
const handleContainerDragLeave = (e: React.DragEvent) => {
|
||||
if (!e.currentTarget.contains(e.relatedTarget)) {
|
||||
setInsertPosition(null);
|
||||
}
|
||||
};
|
||||
|
||||
// 处理项目拖拽经过
|
||||
const handleItemDragOver = (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const rect = e.currentTarget.getBoundingClientRect();
|
||||
const mouseY = e.clientY;
|
||||
const elementMiddle = rect.top + rect.height / 2;
|
||||
|
||||
// 判断鼠标在元素的上半部分还是下半部分
|
||||
const newPosition = mouseY < elementMiddle ? "above" : "below";
|
||||
|
||||
setInsertPosition(newPosition);
|
||||
};
|
||||
|
||||
// 处理项目拖拽离开
|
||||
const handleItemDragLeave = (e: React.DragEvent) => {
|
||||
if (!e.currentTarget.contains(e.relatedTarget)) {
|
||||
setInsertPosition(null);
|
||||
}
|
||||
};
|
||||
|
||||
// 处理放置到空白区域
|
||||
const handleDropToContainer = (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!draggingItem) return;
|
||||
|
||||
// 如果是从算子库拖拽过来的
|
||||
if (draggingSource === "library") {
|
||||
// 检查是否已存在
|
||||
const exists = operators.some((item) => item.id === draggingItem.id);
|
||||
if (!exists) {
|
||||
setOperators([...operators, draggingItem]);
|
||||
}
|
||||
}
|
||||
|
||||
resetDragState();
|
||||
};
|
||||
|
||||
// 处理放置到特定位置
|
||||
const handleItemDrop = (e: React.DragEvent, targetIndex: number) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
if (!draggingItem) return;
|
||||
|
||||
// 从左侧拖拽到右侧的精确插入
|
||||
if (draggingSource === "library") {
|
||||
if (targetIndex !== -1) {
|
||||
const insertIndex =
|
||||
insertPosition === "above" ? targetIndex : targetIndex + 1;
|
||||
|
||||
// 检查是否已存在
|
||||
const exists = operators.some((item) => item.id === draggingItem.id);
|
||||
if (!exists) {
|
||||
const newRightItems = [...operators];
|
||||
newRightItems.splice(insertIndex, 0, draggingItem);
|
||||
|
||||
setOperators(newRightItems);
|
||||
}
|
||||
}
|
||||
}
|
||||
// 右侧容器内的重新排序
|
||||
else if (draggingSource === "sort") {
|
||||
const draggedIndex = operators.findIndex(
|
||||
(item) => item.id === draggingItem.id
|
||||
);
|
||||
if (
|
||||
draggedIndex !== -1 &&
|
||||
targetIndex !== -1 &&
|
||||
draggedIndex !== targetIndex
|
||||
) {
|
||||
const newItems = [...operators];
|
||||
const [draggedItem] = newItems.splice(draggedIndex, 1);
|
||||
|
||||
// 计算正确的插入位置
|
||||
let insertIndex =
|
||||
insertPosition === "above" ? targetIndex : targetIndex + 1;
|
||||
if (draggedIndex < insertIndex) {
|
||||
insertIndex--; // 调整插入位置,因为已经移除了原元素
|
||||
}
|
||||
|
||||
newItems.splice(insertIndex, 0, draggedItem);
|
||||
setOperators(newItems);
|
||||
}
|
||||
}
|
||||
|
||||
resetDragState();
|
||||
};
|
||||
|
||||
// 重置拖拽状态
|
||||
const resetDragState = () => {
|
||||
setDraggingItem(null);
|
||||
setInsertPosition(null);
|
||||
};
|
||||
|
||||
return {
|
||||
handleDragStart,
|
||||
handleDragEnd,
|
||||
handleContainerDragOver,
|
||||
handleContainerDragLeave,
|
||||
handleItemDragOver,
|
||||
handleItemDragLeave,
|
||||
handleItemDrop,
|
||||
handleDropToContainer,
|
||||
};
|
||||
}
|
||||
import { OperatorI } from "@/pages/OperatorMarket/operator.model";
|
||||
import React, { useState } from "react";
|
||||
|
||||
export function useDragOperators({
|
||||
operators,
|
||||
setOperators,
|
||||
}: {
|
||||
operators: OperatorI[];
|
||||
setOperators: (operators: OperatorI[]) => void;
|
||||
}) {
|
||||
const [draggingItem, setDraggingItem] = useState<OperatorI | null>(null);
|
||||
const [draggingSource, setDraggingSource] = useState<
|
||||
"library" | "sort" | null
|
||||
>(null);
|
||||
const [insertPosition, setInsertPosition] = useState<
|
||||
"above" | "below" | null
|
||||
>(null);
|
||||
|
||||
// 处理拖拽开始
|
||||
const handleDragStart = (
|
||||
e: React.DragEvent,
|
||||
item: OperatorI,
|
||||
source: "library" | "sort"
|
||||
) => {
|
||||
setDraggingItem({
|
||||
...item,
|
||||
originalId: item.id,
|
||||
});
|
||||
setDraggingSource(source);
|
||||
e.dataTransfer.effectAllowed = "move";
|
||||
};
|
||||
|
||||
// 处理拖拽结束
|
||||
const handleDragEnd = () => {
|
||||
setDraggingItem(null);
|
||||
setInsertPosition(null);
|
||||
};
|
||||
|
||||
// 处理容器拖拽经过
|
||||
const handleContainerDragOver = (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
// 处理容器拖拽离开
|
||||
const handleContainerDragLeave = (e: React.DragEvent) => {
|
||||
if (!e.currentTarget.contains(e.relatedTarget)) {
|
||||
setInsertPosition(null);
|
||||
}
|
||||
};
|
||||
|
||||
// 处理项目拖拽经过
|
||||
const handleItemDragOver = (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const rect = e.currentTarget.getBoundingClientRect();
|
||||
const mouseY = e.clientY;
|
||||
const elementMiddle = rect.top + rect.height / 2;
|
||||
|
||||
// 判断鼠标在元素的上半部分还是下半部分
|
||||
const newPosition = mouseY < elementMiddle ? "above" : "below";
|
||||
|
||||
setInsertPosition(newPosition);
|
||||
};
|
||||
|
||||
// 处理项目拖拽离开
|
||||
const handleItemDragLeave = (e: React.DragEvent) => {
|
||||
if (!e.currentTarget.contains(e.relatedTarget)) {
|
||||
setInsertPosition(null);
|
||||
}
|
||||
};
|
||||
|
||||
// 处理放置到空白区域
|
||||
const handleDropToContainer = (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!draggingItem) return;
|
||||
|
||||
// 如果是从算子库拖拽过来的
|
||||
if (draggingSource === "library") {
|
||||
// 检查是否已存在
|
||||
const exists = operators.some((item) => item.id === draggingItem.id);
|
||||
if (!exists) {
|
||||
setOperators([...operators, draggingItem]);
|
||||
}
|
||||
}
|
||||
|
||||
resetDragState();
|
||||
};
|
||||
|
||||
// 处理放置到特定位置
|
||||
const handleItemDrop = (e: React.DragEvent, targetIndex: number) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
if (!draggingItem) return;
|
||||
|
||||
// 从左侧拖拽到右侧的精确插入
|
||||
if (draggingSource === "library") {
|
||||
if (targetIndex !== -1) {
|
||||
const insertIndex =
|
||||
insertPosition === "above" ? targetIndex : targetIndex + 1;
|
||||
|
||||
// 检查是否已存在
|
||||
const exists = operators.some((item) => item.id === draggingItem.id);
|
||||
if (!exists) {
|
||||
const newRightItems = [...operators];
|
||||
newRightItems.splice(insertIndex, 0, draggingItem);
|
||||
|
||||
setOperators(newRightItems);
|
||||
}
|
||||
}
|
||||
}
|
||||
// 右侧容器内的重新排序
|
||||
else if (draggingSource === "sort") {
|
||||
const draggedIndex = operators.findIndex(
|
||||
(item) => item.id === draggingItem.id
|
||||
);
|
||||
if (
|
||||
draggedIndex !== -1 &&
|
||||
targetIndex !== -1 &&
|
||||
draggedIndex !== targetIndex
|
||||
) {
|
||||
const newItems = [...operators];
|
||||
const [draggedItem] = newItems.splice(draggedIndex, 1);
|
||||
|
||||
// 计算正确的插入位置
|
||||
let insertIndex =
|
||||
insertPosition === "above" ? targetIndex : targetIndex + 1;
|
||||
if (draggedIndex < insertIndex) {
|
||||
insertIndex--; // 调整插入位置,因为已经移除了原元素
|
||||
}
|
||||
|
||||
newItems.splice(insertIndex, 0, draggedItem);
|
||||
setOperators(newItems);
|
||||
}
|
||||
}
|
||||
|
||||
resetDragState();
|
||||
};
|
||||
|
||||
// 重置拖拽状态
|
||||
const resetDragState = () => {
|
||||
setDraggingItem(null);
|
||||
setInsertPosition(null);
|
||||
};
|
||||
|
||||
return {
|
||||
handleDragStart,
|
||||
handleDragEnd,
|
||||
handleContainerDragOver,
|
||||
handleContainerDragLeave,
|
||||
handleItemDragOver,
|
||||
handleItemDragLeave,
|
||||
handleItemDrop,
|
||||
handleDropToContainer,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,169 +1,169 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { OperatorI } from "@/pages/OperatorMarket/operator.model";
|
||||
import { CleansingTemplate } from "../../cleansing.model";
|
||||
import {queryCleaningTemplateByIdUsingGet, queryCleaningTemplatesUsingGet} from "../../cleansing.api";
|
||||
import {
|
||||
queryCategoryTreeUsingGet,
|
||||
queryOperatorsUsingPost,
|
||||
} from "@/pages/OperatorMarket/operator.api";
|
||||
import {useParams} from "react-router";
|
||||
|
||||
export function useOperatorOperations() {
|
||||
const { id = "" } = useParams();
|
||||
const [currentStep, setCurrentStep] = useState(1);
|
||||
|
||||
const [operators, setOperators] = useState<OperatorI[]>([]);
|
||||
const [selectedOperators, setSelectedOperators] = useState<OperatorI[]>([]);
|
||||
const [configOperator, setConfigOperator] = useState<OperatorI | null>(null);
|
||||
|
||||
const [templates, setTemplates] = useState<CleansingTemplate[]>([]);
|
||||
const [currentTemplate, setCurrentTemplate] =
|
||||
useState<CleansingTemplate | null>(null);
|
||||
|
||||
// 将后端返回的算子数据映射为前端需要的格式
|
||||
const mapOperator = (op: OperatorI) => {
|
||||
const configs =
|
||||
op.settings
|
||||
? JSON.parse(op.settings)
|
||||
: {};
|
||||
const defaultParams: Record<string, string> = {};
|
||||
Object.keys(configs).forEach((key) => {
|
||||
const { value } = configs[key];
|
||||
defaultParams[key] = value;
|
||||
});
|
||||
return {
|
||||
...op,
|
||||
defaultParams,
|
||||
configs,
|
||||
};
|
||||
};
|
||||
|
||||
const [categoryOptions, setCategoryOptions] = useState([]);
|
||||
|
||||
const initOperators = async () => {
|
||||
const [categoryRes, operatorRes] = await Promise.all([
|
||||
queryCategoryTreeUsingGet(),
|
||||
queryOperatorsUsingPost({ page: 0, size: 1000 }),
|
||||
]);
|
||||
|
||||
const operators = operatorRes.data.content.map(mapOperator);
|
||||
setOperators(operators || []);
|
||||
|
||||
const options = categoryRes.data.content.reduce((acc: any[], item: any) => {
|
||||
const cats = item.categories.map((cat) => ({
|
||||
...cat,
|
||||
type: item.name,
|
||||
label: cat.name,
|
||||
value: cat.id,
|
||||
icon: cat.icon,
|
||||
operators: operators.filter((op) => op[item.name] === cat.name),
|
||||
}));
|
||||
acc.push(...cats);
|
||||
return acc;
|
||||
}, [] as { id: string; name: string; icon: React.ReactNode }[]);
|
||||
|
||||
setCategoryOptions(options);
|
||||
};
|
||||
|
||||
const initTemplates = async () => {
|
||||
if (id) {
|
||||
const { data } = await queryCleaningTemplateByIdUsingGet(id);
|
||||
const template = {
|
||||
...data,
|
||||
label: data.name,
|
||||
value: data.id,
|
||||
}
|
||||
setTemplates([template])
|
||||
setCurrentTemplate(template)
|
||||
} else {
|
||||
const { data } = await queryCleaningTemplatesUsingGet();
|
||||
const newTemplates =
|
||||
data.content?.map?.((item) => ({
|
||||
...item,
|
||||
label: item.name,
|
||||
value: item.id,
|
||||
})) || [];
|
||||
setTemplates(newTemplates);
|
||||
setCurrentTemplate(newTemplates?.[0])
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedOperators(currentTemplate?.instance?.map(mapOperator) || []);
|
||||
}, [currentTemplate]);
|
||||
|
||||
useEffect(() => {
|
||||
initTemplates();
|
||||
initOperators();
|
||||
}, []);
|
||||
|
||||
const toggleOperator = (operator: OperatorI) => {
|
||||
const exist = selectedOperators.find((op) => op.id === operator.id);
|
||||
if (exist) {
|
||||
setSelectedOperators(
|
||||
selectedOperators.filter((op) => op.id !== operator.id)
|
||||
);
|
||||
} else {
|
||||
setSelectedOperators([...selectedOperators, { ...operator }]);
|
||||
}
|
||||
};
|
||||
|
||||
// 删除算子
|
||||
const removeOperator = (id: string) => {
|
||||
setSelectedOperators(selectedOperators.filter((op) => op.id !== id));
|
||||
if (configOperator?.id === id) setConfigOperator(null);
|
||||
};
|
||||
|
||||
// 配置算子参数变化
|
||||
const handleConfigChange = (
|
||||
operatorId: string,
|
||||
paramKey: string,
|
||||
value: any
|
||||
) => {
|
||||
setSelectedOperators((prev) =>
|
||||
prev.map((op) =>
|
||||
op.id === operatorId
|
||||
? {
|
||||
...op,
|
||||
overrides: {
|
||||
...(op?.overrides || op?.defaultParams),
|
||||
[paramKey]: value,
|
||||
},
|
||||
}
|
||||
: op
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const handleNext = () => {
|
||||
if (currentStep < 2) {
|
||||
setCurrentStep(currentStep + 1);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePrev = () => {
|
||||
if (currentStep > 1) {
|
||||
setCurrentStep(currentStep - 1);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
currentStep,
|
||||
templates,
|
||||
currentTemplate,
|
||||
configOperator,
|
||||
categoryOptions,
|
||||
setConfigOperator,
|
||||
setCurrentTemplate,
|
||||
setCurrentStep,
|
||||
operators,
|
||||
setOperators,
|
||||
selectedOperators,
|
||||
setSelectedOperators,
|
||||
handleConfigChange,
|
||||
toggleOperator,
|
||||
removeOperator,
|
||||
handleNext,
|
||||
handlePrev,
|
||||
};
|
||||
}
|
||||
import { useEffect, useState } from "react";
|
||||
import { OperatorI } from "@/pages/OperatorMarket/operator.model";
|
||||
import { CleansingTemplate } from "../../cleansing.model";
|
||||
import {queryCleaningTemplateByIdUsingGet, queryCleaningTemplatesUsingGet} from "../../cleansing.api";
|
||||
import {
|
||||
queryCategoryTreeUsingGet,
|
||||
queryOperatorsUsingPost,
|
||||
} from "@/pages/OperatorMarket/operator.api";
|
||||
import {useParams} from "react-router";
|
||||
|
||||
export function useOperatorOperations() {
|
||||
const { id = "" } = useParams();
|
||||
const [currentStep, setCurrentStep] = useState(1);
|
||||
|
||||
const [operators, setOperators] = useState<OperatorI[]>([]);
|
||||
const [selectedOperators, setSelectedOperators] = useState<OperatorI[]>([]);
|
||||
const [configOperator, setConfigOperator] = useState<OperatorI | null>(null);
|
||||
|
||||
const [templates, setTemplates] = useState<CleansingTemplate[]>([]);
|
||||
const [currentTemplate, setCurrentTemplate] =
|
||||
useState<CleansingTemplate | null>(null);
|
||||
|
||||
// 将后端返回的算子数据映射为前端需要的格式
|
||||
const mapOperator = (op: OperatorI) => {
|
||||
const configs =
|
||||
op.settings
|
||||
? JSON.parse(op.settings)
|
||||
: {};
|
||||
const defaultParams: Record<string, string> = {};
|
||||
Object.keys(configs).forEach((key) => {
|
||||
const { value } = configs[key];
|
||||
defaultParams[key] = value;
|
||||
});
|
||||
return {
|
||||
...op,
|
||||
defaultParams,
|
||||
configs,
|
||||
};
|
||||
};
|
||||
|
||||
const [categoryOptions, setCategoryOptions] = useState([]);
|
||||
|
||||
const initOperators = async () => {
|
||||
const [categoryRes, operatorRes] = await Promise.all([
|
||||
queryCategoryTreeUsingGet(),
|
||||
queryOperatorsUsingPost({ page: 0, size: 1000 }),
|
||||
]);
|
||||
|
||||
const operators = operatorRes.data.content.map(mapOperator);
|
||||
setOperators(operators || []);
|
||||
|
||||
const options = categoryRes.data.content.reduce((acc: any[], item: any) => {
|
||||
const cats = item.categories.map((cat) => ({
|
||||
...cat,
|
||||
type: item.name,
|
||||
label: cat.name,
|
||||
value: cat.id,
|
||||
icon: cat.icon,
|
||||
operators: operators.filter((op) => op[item.name] === cat.name),
|
||||
}));
|
||||
acc.push(...cats);
|
||||
return acc;
|
||||
}, [] as { id: string; name: string; icon: React.ReactNode }[]);
|
||||
|
||||
setCategoryOptions(options);
|
||||
};
|
||||
|
||||
const initTemplates = async () => {
|
||||
if (id) {
|
||||
const { data } = await queryCleaningTemplateByIdUsingGet(id);
|
||||
const template = {
|
||||
...data,
|
||||
label: data.name,
|
||||
value: data.id,
|
||||
}
|
||||
setTemplates([template])
|
||||
setCurrentTemplate(template)
|
||||
} else {
|
||||
const { data } = await queryCleaningTemplatesUsingGet();
|
||||
const newTemplates =
|
||||
data.content?.map?.((item) => ({
|
||||
...item,
|
||||
label: item.name,
|
||||
value: item.id,
|
||||
})) || [];
|
||||
setTemplates(newTemplates);
|
||||
setCurrentTemplate(newTemplates?.[0])
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedOperators(currentTemplate?.instance?.map(mapOperator) || []);
|
||||
}, [currentTemplate]);
|
||||
|
||||
useEffect(() => {
|
||||
initTemplates();
|
||||
initOperators();
|
||||
}, []);
|
||||
|
||||
const toggleOperator = (operator: OperatorI) => {
|
||||
const exist = selectedOperators.find((op) => op.id === operator.id);
|
||||
if (exist) {
|
||||
setSelectedOperators(
|
||||
selectedOperators.filter((op) => op.id !== operator.id)
|
||||
);
|
||||
} else {
|
||||
setSelectedOperators([...selectedOperators, { ...operator }]);
|
||||
}
|
||||
};
|
||||
|
||||
// 删除算子
|
||||
const removeOperator = (id: string) => {
|
||||
setSelectedOperators(selectedOperators.filter((op) => op.id !== id));
|
||||
if (configOperator?.id === id) setConfigOperator(null);
|
||||
};
|
||||
|
||||
// 配置算子参数变化
|
||||
const handleConfigChange = (
|
||||
operatorId: string,
|
||||
paramKey: string,
|
||||
value: any
|
||||
) => {
|
||||
setSelectedOperators((prev) =>
|
||||
prev.map((op) =>
|
||||
op.id === operatorId
|
||||
? {
|
||||
...op,
|
||||
overrides: {
|
||||
...(op?.overrides || op?.defaultParams),
|
||||
[paramKey]: value,
|
||||
},
|
||||
}
|
||||
: op
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const handleNext = () => {
|
||||
if (currentStep < 2) {
|
||||
setCurrentStep(currentStep + 1);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePrev = () => {
|
||||
if (currentStep > 1) {
|
||||
setCurrentStep(currentStep - 1);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
currentStep,
|
||||
templates,
|
||||
currentTemplate,
|
||||
configOperator,
|
||||
categoryOptions,
|
||||
setConfigOperator,
|
||||
setCurrentTemplate,
|
||||
setCurrentStep,
|
||||
operators,
|
||||
setOperators,
|
||||
selectedOperators,
|
||||
setSelectedOperators,
|
||||
handleConfigChange,
|
||||
toggleOperator,
|
||||
removeOperator,
|
||||
handleNext,
|
||||
handlePrev,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,223 +1,223 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import {Breadcrumb, App, Tabs} from "antd";
|
||||
import {
|
||||
Play,
|
||||
Pause,
|
||||
Clock,
|
||||
CheckCircle,
|
||||
AlertCircle,
|
||||
Trash2,
|
||||
Activity, LayoutList,
|
||||
} from "lucide-react";
|
||||
import DetailHeader from "@/components/DetailHeader";
|
||||
import { Link, useNavigate, useParams } from "react-router";
|
||||
import {
|
||||
deleteCleaningTaskByIdUsingDelete,
|
||||
executeCleaningTaskUsingPost,
|
||||
queryCleaningTaskByIdUsingGet, queryCleaningTaskLogByIdUsingGet, queryCleaningTaskResultByIdUsingGet,
|
||||
stopCleaningTaskUsingPost,
|
||||
} from "../cleansing.api";
|
||||
import {mapTask, TaskStatusMap} from "../cleansing.const";
|
||||
import {CleansingResult, TaskStatus} from "@/pages/DataCleansing/cleansing.model";
|
||||
import BasicInfo from "./components/BasicInfo";
|
||||
import OperatorTable from "./components/OperatorTable";
|
||||
import FileTable from "./components/FileTable";
|
||||
import LogsTable from "./components/LogsTable";
|
||||
import {formatExecutionDuration} from "@/utils/unit.ts";
|
||||
import {ReloadOutlined} from "@ant-design/icons";
|
||||
|
||||
// 任务详情页面组件
|
||||
export default function CleansingTaskDetail() {
|
||||
const { id = "" } = useParams(); // 获取动态路由参数
|
||||
const { message } = App.useApp();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const fetchTaskDetail = async () => {
|
||||
if (!id) return;
|
||||
try {
|
||||
const { data } = await queryCleaningTaskByIdUsingGet(id);
|
||||
setTask(mapTask(data));
|
||||
} catch (error) {
|
||||
message.error("获取任务详情失败");
|
||||
navigate("/data/cleansing");
|
||||
}
|
||||
};
|
||||
|
||||
const pauseTask = async () => {
|
||||
await stopCleaningTaskUsingPost(id);
|
||||
message.success("任务已暂停");
|
||||
fetchTaskDetail();
|
||||
};
|
||||
|
||||
const startTask = async () => {
|
||||
await executeCleaningTaskUsingPost(id);
|
||||
message.success("任务已启动");
|
||||
fetchTaskDetail();
|
||||
};
|
||||
|
||||
const deleteTask = async () => {
|
||||
await deleteCleaningTaskByIdUsingDelete(id);
|
||||
message.success("任务已删除");
|
||||
navigate("/data/cleansing");
|
||||
};
|
||||
|
||||
const [result, setResult] = useState<CleansingResult[]>();
|
||||
|
||||
const fetchTaskResult = async () => {
|
||||
if (!id) return;
|
||||
try {
|
||||
const { data } = await queryCleaningTaskResultByIdUsingGet(id);
|
||||
setResult(data);
|
||||
} catch (error) {
|
||||
message.error("获取清洗结果失败");
|
||||
navigate("/data/cleansing/task-detail/" + id);
|
||||
}
|
||||
};
|
||||
|
||||
const [taskLog, setTaskLog] = useState();
|
||||
|
||||
const fetchTaskLog = async () => {
|
||||
if (!id) return;
|
||||
try {
|
||||
const { data } = await queryCleaningTaskLogByIdUsingGet(id);
|
||||
setTaskLog(data);
|
||||
} catch (error) {
|
||||
message.error("获取清洗日志失败");
|
||||
navigate("/data/cleansing/task-detail/" + id);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRefresh = async () => {
|
||||
fetchTaskDetail();
|
||||
{activeTab === "files" && await fetchTaskResult()}
|
||||
{activeTab === "logs" && await fetchTaskLog()}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchTaskDetail();
|
||||
}, [id]);
|
||||
|
||||
const [task, setTask] = useState(null);
|
||||
const [activeTab, setActiveTab] = useState("basic");
|
||||
|
||||
const headerData = {
|
||||
...task,
|
||||
icon: <LayoutList className="w-8 h-8" />,
|
||||
status: TaskStatusMap[task?.status],
|
||||
createdAt: task?.createdAt,
|
||||
lastUpdated: task?.updatedAt,
|
||||
};
|
||||
|
||||
const statistics = [
|
||||
{
|
||||
icon: <Clock className="w-4 h-4 text-blue-500" />,
|
||||
label: "总耗时",
|
||||
value: formatExecutionDuration(task?.startedAt, task?.finishedAt) || "--",
|
||||
},
|
||||
{
|
||||
icon: <CheckCircle className="w-4 h-4 text-green-500" />,
|
||||
label: "成功文件",
|
||||
value: task?.progress?.succeedFileNum || "0",
|
||||
},
|
||||
{
|
||||
icon: <AlertCircle className="w-4 h-4 text-red-500" />,
|
||||
label: "失败文件",
|
||||
value: (task?.status.value === TaskStatus.RUNNING || task?.status.value === TaskStatus.PENDING) ?
|
||||
task?.progress.failedFileNum :
|
||||
task?.progress?.totalFileNum - task?.progress.succeedFileNum,
|
||||
},
|
||||
{
|
||||
icon: <Activity className="w-4 h-4 text-purple-500" />,
|
||||
label: "成功率",
|
||||
value: task?.progress?.successRate ? task?.progress?.successRate + "%" : "--",
|
||||
},
|
||||
];
|
||||
|
||||
const operations = [
|
||||
...(task?.status === TaskStatus.RUNNING
|
||||
? [
|
||||
{
|
||||
key: "pause",
|
||||
label: "暂停任务",
|
||||
icon: <Pause className="w-4 h-4" />,
|
||||
onClick: pauseTask,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...([TaskStatus.PENDING, TaskStatus.STOPPED, TaskStatus.FAILED].includes(task?.status?.value)
|
||||
? [
|
||||
{
|
||||
key: "start",
|
||||
label: "执行任务",
|
||||
icon: <Play className="w-4 h-4" />,
|
||||
onClick: startTask,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
key: "refresh",
|
||||
label: "更新任务",
|
||||
icon: <ReloadOutlined className="w-4 h-4" />,
|
||||
onClick: handleRefresh,
|
||||
},
|
||||
{
|
||||
key: "delete",
|
||||
label: "删除任务",
|
||||
icon: <Trash2 className="w-4 h-4" />,
|
||||
danger: true,
|
||||
onClick: deleteTask,
|
||||
},
|
||||
];
|
||||
|
||||
const tabList = [
|
||||
{
|
||||
key: "basic",
|
||||
label: "基本信息",
|
||||
},
|
||||
{
|
||||
key: "operators",
|
||||
label: "处理算子",
|
||||
},
|
||||
{
|
||||
key: "files",
|
||||
label: "处理文件",
|
||||
},
|
||||
{
|
||||
key: "logs",
|
||||
label: "运行日志",
|
||||
},
|
||||
];
|
||||
|
||||
const breadItems = [
|
||||
{
|
||||
title: <Link to="/data/cleansing">数据清洗</Link>,
|
||||
},
|
||||
{
|
||||
title: "清洗任务详情",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<Breadcrumb items={breadItems} />
|
||||
<div className="mb-4 mt-4">
|
||||
<DetailHeader
|
||||
data={headerData}
|
||||
statistics={statistics}
|
||||
operations={operations}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-overflow-auto p-6 pt-2 bg-white rounded-md shadow">
|
||||
<Tabs activeKey={activeTab} items={tabList} onChange={setActiveTab} />
|
||||
<div className="h-full flex-1 overflow-auto">
|
||||
{activeTab === "basic" && (
|
||||
<BasicInfo task={task} />
|
||||
)}
|
||||
{activeTab === "operators" && <OperatorTable task={task} />}
|
||||
{activeTab === "files" && <FileTable result={result} fetchTaskResult={fetchTaskResult} />}
|
||||
{activeTab === "logs" && <LogsTable taskLog={taskLog} fetchTaskLog={fetchTaskLog} />}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
import { useEffect, useState } from "react";
|
||||
import {Breadcrumb, App, Tabs} from "antd";
|
||||
import {
|
||||
Play,
|
||||
Pause,
|
||||
Clock,
|
||||
CheckCircle,
|
||||
AlertCircle,
|
||||
Trash2,
|
||||
Activity, LayoutList,
|
||||
} from "lucide-react";
|
||||
import DetailHeader from "@/components/DetailHeader";
|
||||
import { Link, useNavigate, useParams } from "react-router";
|
||||
import {
|
||||
deleteCleaningTaskByIdUsingDelete,
|
||||
executeCleaningTaskUsingPost,
|
||||
queryCleaningTaskByIdUsingGet, queryCleaningTaskLogByIdUsingGet, queryCleaningTaskResultByIdUsingGet,
|
||||
stopCleaningTaskUsingPost,
|
||||
} from "../cleansing.api";
|
||||
import {mapTask, TaskStatusMap} from "../cleansing.const";
|
||||
import {CleansingResult, TaskStatus} from "@/pages/DataCleansing/cleansing.model";
|
||||
import BasicInfo from "./components/BasicInfo";
|
||||
import OperatorTable from "./components/OperatorTable";
|
||||
import FileTable from "./components/FileTable";
|
||||
import LogsTable from "./components/LogsTable";
|
||||
import {formatExecutionDuration} from "@/utils/unit.ts";
|
||||
import {ReloadOutlined} from "@ant-design/icons";
|
||||
|
||||
// 任务详情页面组件
|
||||
export default function CleansingTaskDetail() {
|
||||
const { id = "" } = useParams(); // 获取动态路由参数
|
||||
const { message } = App.useApp();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const fetchTaskDetail = async () => {
|
||||
if (!id) return;
|
||||
try {
|
||||
const { data } = await queryCleaningTaskByIdUsingGet(id);
|
||||
setTask(mapTask(data));
|
||||
} catch (error) {
|
||||
message.error("获取任务详情失败");
|
||||
navigate("/data/cleansing");
|
||||
}
|
||||
};
|
||||
|
||||
const pauseTask = async () => {
|
||||
await stopCleaningTaskUsingPost(id);
|
||||
message.success("任务已暂停");
|
||||
fetchTaskDetail();
|
||||
};
|
||||
|
||||
const startTask = async () => {
|
||||
await executeCleaningTaskUsingPost(id);
|
||||
message.success("任务已启动");
|
||||
fetchTaskDetail();
|
||||
};
|
||||
|
||||
const deleteTask = async () => {
|
||||
await deleteCleaningTaskByIdUsingDelete(id);
|
||||
message.success("任务已删除");
|
||||
navigate("/data/cleansing");
|
||||
};
|
||||
|
||||
const [result, setResult] = useState<CleansingResult[]>();
|
||||
|
||||
const fetchTaskResult = async () => {
|
||||
if (!id) return;
|
||||
try {
|
||||
const { data } = await queryCleaningTaskResultByIdUsingGet(id);
|
||||
setResult(data);
|
||||
} catch (error) {
|
||||
message.error("获取清洗结果失败");
|
||||
navigate("/data/cleansing/task-detail/" + id);
|
||||
}
|
||||
};
|
||||
|
||||
const [taskLog, setTaskLog] = useState();
|
||||
|
||||
const fetchTaskLog = async () => {
|
||||
if (!id) return;
|
||||
try {
|
||||
const { data } = await queryCleaningTaskLogByIdUsingGet(id);
|
||||
setTaskLog(data);
|
||||
} catch (error) {
|
||||
message.error("获取清洗日志失败");
|
||||
navigate("/data/cleansing/task-detail/" + id);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRefresh = async () => {
|
||||
fetchTaskDetail();
|
||||
{activeTab === "files" && await fetchTaskResult()}
|
||||
{activeTab === "logs" && await fetchTaskLog()}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchTaskDetail();
|
||||
}, [id]);
|
||||
|
||||
const [task, setTask] = useState(null);
|
||||
const [activeTab, setActiveTab] = useState("basic");
|
||||
|
||||
const headerData = {
|
||||
...task,
|
||||
icon: <LayoutList className="w-8 h-8" />,
|
||||
status: TaskStatusMap[task?.status],
|
||||
createdAt: task?.createdAt,
|
||||
lastUpdated: task?.updatedAt,
|
||||
};
|
||||
|
||||
const statistics = [
|
||||
{
|
||||
icon: <Clock className="w-4 h-4 text-blue-500" />,
|
||||
label: "总耗时",
|
||||
value: formatExecutionDuration(task?.startedAt, task?.finishedAt) || "--",
|
||||
},
|
||||
{
|
||||
icon: <CheckCircle className="w-4 h-4 text-green-500" />,
|
||||
label: "成功文件",
|
||||
value: task?.progress?.succeedFileNum || "0",
|
||||
},
|
||||
{
|
||||
icon: <AlertCircle className="w-4 h-4 text-red-500" />,
|
||||
label: "失败文件",
|
||||
value: (task?.status.value === TaskStatus.RUNNING || task?.status.value === TaskStatus.PENDING) ?
|
||||
task?.progress.failedFileNum :
|
||||
task?.progress?.totalFileNum - task?.progress.succeedFileNum,
|
||||
},
|
||||
{
|
||||
icon: <Activity className="w-4 h-4 text-purple-500" />,
|
||||
label: "成功率",
|
||||
value: task?.progress?.successRate ? task?.progress?.successRate + "%" : "--",
|
||||
},
|
||||
];
|
||||
|
||||
const operations = [
|
||||
...(task?.status === TaskStatus.RUNNING
|
||||
? [
|
||||
{
|
||||
key: "pause",
|
||||
label: "暂停任务",
|
||||
icon: <Pause className="w-4 h-4" />,
|
||||
onClick: pauseTask,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...([TaskStatus.PENDING, TaskStatus.STOPPED, TaskStatus.FAILED].includes(task?.status?.value)
|
||||
? [
|
||||
{
|
||||
key: "start",
|
||||
label: "执行任务",
|
||||
icon: <Play className="w-4 h-4" />,
|
||||
onClick: startTask,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
key: "refresh",
|
||||
label: "更新任务",
|
||||
icon: <ReloadOutlined className="w-4 h-4" />,
|
||||
onClick: handleRefresh,
|
||||
},
|
||||
{
|
||||
key: "delete",
|
||||
label: "删除任务",
|
||||
icon: <Trash2 className="w-4 h-4" />,
|
||||
danger: true,
|
||||
onClick: deleteTask,
|
||||
},
|
||||
];
|
||||
|
||||
const tabList = [
|
||||
{
|
||||
key: "basic",
|
||||
label: "基本信息",
|
||||
},
|
||||
{
|
||||
key: "operators",
|
||||
label: "处理算子",
|
||||
},
|
||||
{
|
||||
key: "files",
|
||||
label: "处理文件",
|
||||
},
|
||||
{
|
||||
key: "logs",
|
||||
label: "运行日志",
|
||||
},
|
||||
];
|
||||
|
||||
const breadItems = [
|
||||
{
|
||||
title: <Link to="/data/cleansing">数据清洗</Link>,
|
||||
},
|
||||
{
|
||||
title: "清洗任务详情",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<Breadcrumb items={breadItems} />
|
||||
<div className="mb-4 mt-4">
|
||||
<DetailHeader
|
||||
data={headerData}
|
||||
statistics={statistics}
|
||||
operations={operations}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-overflow-auto p-6 pt-2 bg-white rounded-md shadow">
|
||||
<Tabs activeKey={activeTab} items={tabList} onChange={setActiveTab} />
|
||||
<div className="h-full flex-1 overflow-auto">
|
||||
{activeTab === "basic" && (
|
||||
<BasicInfo task={task} />
|
||||
)}
|
||||
{activeTab === "operators" && <OperatorTable task={task} />}
|
||||
{activeTab === "files" && <FileTable result={result} fetchTaskResult={fetchTaskResult} />}
|
||||
{activeTab === "logs" && <LogsTable taskLog={taskLog} fetchTaskLog={fetchTaskLog} />}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,122 +1,122 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import {Breadcrumb, App, Tabs} from "antd";
|
||||
import {
|
||||
Trash2,
|
||||
LayoutList,
|
||||
} from "lucide-react";
|
||||
import DetailHeader from "@/components/DetailHeader";
|
||||
import { Link, useNavigate, useParams } from "react-router";
|
||||
import {
|
||||
deleteCleaningTemplateByIdUsingDelete,
|
||||
queryCleaningTemplateByIdUsingGet,
|
||||
} from "../cleansing.api";
|
||||
import {mapTemplate} from "../cleansing.const";
|
||||
import OperatorTable from "./components/OperatorTable";
|
||||
import {EditOutlined, ReloadOutlined, NumberOutlined} from "@ant-design/icons";
|
||||
|
||||
// 任务详情页面组件
|
||||
export default function CleansingTemplateDetail() {
|
||||
const { id = "" } = useParams(); // 获取动态路由参数
|
||||
const { message } = App.useApp();
|
||||
const navigate = useNavigate();
|
||||
const [template, setTemplate] = useState();
|
||||
|
||||
const fetchTemplateDetail = async () => {
|
||||
if (!id) return;
|
||||
try {
|
||||
const { data } = await queryCleaningTemplateByIdUsingGet(id);
|
||||
setTemplate(mapTemplate(data));
|
||||
} catch (error) {
|
||||
message.error("获取任务详情失败");
|
||||
navigate("/data/cleansing");
|
||||
}
|
||||
};
|
||||
|
||||
const deleteTemplate = async () => {
|
||||
await deleteCleaningTemplateByIdUsingDelete(id);
|
||||
message.success("模板已删除");
|
||||
navigate("/data/cleansing");
|
||||
};
|
||||
|
||||
const handleRefresh = async () => {
|
||||
fetchTemplateDetail();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchTemplateDetail();
|
||||
}, [id]);
|
||||
|
||||
const [activeTab, setActiveTab] = useState("operators");
|
||||
|
||||
const headerData = {
|
||||
...template,
|
||||
icon: <LayoutList className="w-8 h-8" />,
|
||||
createdAt: template?.createdAt,
|
||||
lastUpdated: template?.updatedAt,
|
||||
};
|
||||
|
||||
const statistics = [
|
||||
{
|
||||
icon: <NumberOutlined className="w-4 h-4 text-green-500" />,
|
||||
label: "算子数量",
|
||||
value: template?.instance?.length || 0,
|
||||
},
|
||||
];
|
||||
|
||||
const operations = [
|
||||
{
|
||||
key: "update",
|
||||
label: "更新任务",
|
||||
icon: <EditOutlined className="w-4 h-4" />,
|
||||
onClick: () => navigate(`/data/cleansing/update-template/${id}`),
|
||||
},
|
||||
{
|
||||
key: "refresh",
|
||||
label: "更新任务",
|
||||
icon: <ReloadOutlined className="w-4 h-4" />,
|
||||
onClick: handleRefresh,
|
||||
},
|
||||
{
|
||||
key: "delete",
|
||||
label: "删除任务",
|
||||
icon: <Trash2 className="w-4 h-4" />,
|
||||
danger: true,
|
||||
onClick: deleteTemplate,
|
||||
},
|
||||
];
|
||||
|
||||
const tabList = [
|
||||
{
|
||||
key: "operators",
|
||||
label: "处理算子",
|
||||
},
|
||||
];
|
||||
|
||||
const breadItems = [
|
||||
{
|
||||
title: <Link to="/data/cleansing">数据清洗</Link>,
|
||||
},
|
||||
{
|
||||
title: "模板详情",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<Breadcrumb items={breadItems} />
|
||||
<div className="mb-4 mt-4">
|
||||
<DetailHeader
|
||||
data={headerData}
|
||||
statistics={statistics}
|
||||
operations={operations}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-overflow-auto p-6 pt-2 bg-white rounded-md shadow">
|
||||
<Tabs activeKey={activeTab} items={tabList} onChange={setActiveTab} />
|
||||
<div className="h-full flex-1 overflow-auto">
|
||||
<OperatorTable task={template} />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
import { useEffect, useState } from "react";
|
||||
import {Breadcrumb, App, Tabs} from "antd";
|
||||
import {
|
||||
Trash2,
|
||||
LayoutList,
|
||||
} from "lucide-react";
|
||||
import DetailHeader from "@/components/DetailHeader";
|
||||
import { Link, useNavigate, useParams } from "react-router";
|
||||
import {
|
||||
deleteCleaningTemplateByIdUsingDelete,
|
||||
queryCleaningTemplateByIdUsingGet,
|
||||
} from "../cleansing.api";
|
||||
import {mapTemplate} from "../cleansing.const";
|
||||
import OperatorTable from "./components/OperatorTable";
|
||||
import {EditOutlined, ReloadOutlined, NumberOutlined} from "@ant-design/icons";
|
||||
|
||||
// 任务详情页面组件
|
||||
export default function CleansingTemplateDetail() {
|
||||
const { id = "" } = useParams(); // 获取动态路由参数
|
||||
const { message } = App.useApp();
|
||||
const navigate = useNavigate();
|
||||
const [template, setTemplate] = useState();
|
||||
|
||||
const fetchTemplateDetail = async () => {
|
||||
if (!id) return;
|
||||
try {
|
||||
const { data } = await queryCleaningTemplateByIdUsingGet(id);
|
||||
setTemplate(mapTemplate(data));
|
||||
} catch (error) {
|
||||
message.error("获取任务详情失败");
|
||||
navigate("/data/cleansing");
|
||||
}
|
||||
};
|
||||
|
||||
const deleteTemplate = async () => {
|
||||
await deleteCleaningTemplateByIdUsingDelete(id);
|
||||
message.success("模板已删除");
|
||||
navigate("/data/cleansing");
|
||||
};
|
||||
|
||||
const handleRefresh = async () => {
|
||||
fetchTemplateDetail();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchTemplateDetail();
|
||||
}, [id]);
|
||||
|
||||
const [activeTab, setActiveTab] = useState("operators");
|
||||
|
||||
const headerData = {
|
||||
...template,
|
||||
icon: <LayoutList className="w-8 h-8" />,
|
||||
createdAt: template?.createdAt,
|
||||
lastUpdated: template?.updatedAt,
|
||||
};
|
||||
|
||||
const statistics = [
|
||||
{
|
||||
icon: <NumberOutlined className="w-4 h-4 text-green-500" />,
|
||||
label: "算子数量",
|
||||
value: template?.instance?.length || 0,
|
||||
},
|
||||
];
|
||||
|
||||
const operations = [
|
||||
{
|
||||
key: "update",
|
||||
label: "更新任务",
|
||||
icon: <EditOutlined className="w-4 h-4" />,
|
||||
onClick: () => navigate(`/data/cleansing/update-template/${id}`),
|
||||
},
|
||||
{
|
||||
key: "refresh",
|
||||
label: "更新任务",
|
||||
icon: <ReloadOutlined className="w-4 h-4" />,
|
||||
onClick: handleRefresh,
|
||||
},
|
||||
{
|
||||
key: "delete",
|
||||
label: "删除任务",
|
||||
icon: <Trash2 className="w-4 h-4" />,
|
||||
danger: true,
|
||||
onClick: deleteTemplate,
|
||||
},
|
||||
];
|
||||
|
||||
const tabList = [
|
||||
{
|
||||
key: "operators",
|
||||
label: "处理算子",
|
||||
},
|
||||
];
|
||||
|
||||
const breadItems = [
|
||||
{
|
||||
title: <Link to="/data/cleansing">数据清洗</Link>,
|
||||
},
|
||||
{
|
||||
title: "模板详情",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<Breadcrumb items={breadItems} />
|
||||
<div className="mb-4 mt-4">
|
||||
<DetailHeader
|
||||
data={headerData}
|
||||
statistics={statistics}
|
||||
operations={operations}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-overflow-auto p-6 pt-2 bg-white rounded-md shadow">
|
||||
<Tabs activeKey={activeTab} items={tabList} onChange={setActiveTab} />
|
||||
<div className="h-full flex-1 overflow-auto">
|
||||
<OperatorTable task={template} />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,138 +1,138 @@
|
||||
import {CleansingTask, TaskStatus} from "@/pages/DataCleansing/cleansing.model";
|
||||
import { Button, Card, Descriptions, Progress } from "antd";
|
||||
import { Activity, AlertCircle, CheckCircle, Clock } from "lucide-react";
|
||||
import { useNavigate } from "react-router";
|
||||
import {formatExecutionDuration} from "@/utils/unit.ts";
|
||||
|
||||
export default function BasicInfo({ task }: { task: CleansingTask }) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const descriptionItems = [
|
||||
{
|
||||
key: "id",
|
||||
label: "任务ID",
|
||||
children: <span className="font-mono">{task?.id}</span>,
|
||||
},
|
||||
{ key: "name", label: "任务名称", children: task?.name },
|
||||
{
|
||||
key: "dataset",
|
||||
label: "源数据集",
|
||||
children: (
|
||||
<Button
|
||||
style={{ paddingLeft: 0, marginLeft: 0 }}
|
||||
type="link"
|
||||
size="small"
|
||||
onClick={() =>
|
||||
navigate("/data/management/detail/" + task?.srcDatasetId)
|
||||
}
|
||||
>
|
||||
{task?.srcDatasetName}
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "targetDataset",
|
||||
label: "目标数据集",
|
||||
children: (
|
||||
<Button
|
||||
style={{ paddingLeft: 0, marginLeft: 0 }}
|
||||
type="link"
|
||||
size="small"
|
||||
onClick={() =>
|
||||
navigate("/data/management/detail/" + task?.destDatasetId)
|
||||
}
|
||||
>
|
||||
{task?.destDatasetName}
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
{ key: "startTime", label: "开始时间", children: task?.startedAt },
|
||||
{
|
||||
key: "description",
|
||||
label: "任务描述",
|
||||
children: (
|
||||
<span className="text-gray-600">{task?.description || "--"}</span>
|
||||
),
|
||||
span: 2,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* 执行摘要 */}
|
||||
<Card className="mb-6">
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
<div className="text-center p-4 bg-gradient-to-br from-blue-50 to-blue-100 rounded-lg">
|
||||
<Clock className="w-8 h-8 text-blue-500 mb-2 mx-auto" />
|
||||
<div className="text-xl font-bold text-blue-500">
|
||||
{formatExecutionDuration(task?.startedAt, task?.finishedAt) || "--"}
|
||||
</div>
|
||||
<div className="text-sm text-gray-600">总耗时</div>
|
||||
</div>
|
||||
<div className="text-center p-4 bg-gradient-to-br from-green-50 to-green-100 rounded-lg">
|
||||
<CheckCircle className="w-8 h-8 text-green-500 mb-2 mx-auto" />
|
||||
<div className="text-xl font-bold text-green-500">
|
||||
{task?.progress?.succeedFileNum || "0"}
|
||||
</div>
|
||||
<div className="text-sm text-gray-600">成功文件</div>
|
||||
</div>
|
||||
<div className="text-center p-4 bg-gradient-to-br from-red-50 to-red-100 rounded-lg">
|
||||
<AlertCircle className="w-8 h-8 text-red-500 mb-2 mx-auto" />
|
||||
<div className="text-xl font-bold text-red-500">
|
||||
{(task?.status.value === TaskStatus.RUNNING || task?.status.value === TaskStatus.PENDING) ?
|
||||
task?.progress.failedFileNum :
|
||||
task?.progress?.totalFileNum - task?.progress.succeedFileNum}
|
||||
</div>
|
||||
<div className="text-sm text-gray-600">失败文件</div>
|
||||
</div>
|
||||
<div className="text-center p-4 bg-gradient-to-br from-purple-50 to-purple-100 rounded-lg">
|
||||
<Activity className="w-8 h-8 text-purple-500 mb-2 mx-auto" />
|
||||
<div className="text-xl font-bold text-purple-500">
|
||||
{task?.progress?.successRate ? task?.progress?.successRate + "%" : "--"}
|
||||
</div>
|
||||
<div className="text-sm text-gray-600">成功率</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
{/* 基本信息 */}
|
||||
<Card>
|
||||
<div className="mb-8">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">基本信息</h3>
|
||||
<Descriptions
|
||||
column={2}
|
||||
bordered={false}
|
||||
size="middle"
|
||||
labelStyle={{ fontWeight: 500, color: "#555" }}
|
||||
contentStyle={{ fontSize: 14 }}
|
||||
items={descriptionItems}
|
||||
></Descriptions>
|
||||
</div>
|
||||
{/* 处理进度 */}
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">处理进度</h3>
|
||||
{ task?.status?.value === TaskStatus.FAILED ?
|
||||
<Progress percent={task?.progress?.process} size="small" status="exception" />
|
||||
: <Progress percent={task?.progress?.process} size="small"/>
|
||||
}
|
||||
<div className="grid grid-cols-2 gap-4 text-sm mt-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-3 h-3 bg-green-500 rounded-full inline-block" />
|
||||
<span>已完成: {task?.progress?.succeedFileNum || "0"}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-3 h-3 bg-blue-500 rounded-full inline-block" />
|
||||
<span>处理中: {(task?.status.value === TaskStatus.RUNNING || task?.status.value === TaskStatus.PENDING) ?
|
||||
task?.progress?.totalFileNum - task?.progress.succeedFileNum : 0}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-3 h-3 bg-red-500 rounded-full inline-block" />
|
||||
<span>失败: {(task?.status.value === TaskStatus.RUNNING || task?.status.value === TaskStatus.PENDING) ?
|
||||
task?.progress.failedFileNum :
|
||||
task?.progress?.totalFileNum - task?.progress.succeedFileNum}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</>
|
||||
);
|
||||
}
|
||||
import {CleansingTask, TaskStatus} from "@/pages/DataCleansing/cleansing.model";
|
||||
import { Button, Card, Descriptions, Progress } from "antd";
|
||||
import { Activity, AlertCircle, CheckCircle, Clock } from "lucide-react";
|
||||
import { useNavigate } from "react-router";
|
||||
import {formatExecutionDuration} from "@/utils/unit.ts";
|
||||
|
||||
export default function BasicInfo({ task }: { task: CleansingTask }) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const descriptionItems = [
|
||||
{
|
||||
key: "id",
|
||||
label: "任务ID",
|
||||
children: <span className="font-mono">{task?.id}</span>,
|
||||
},
|
||||
{ key: "name", label: "任务名称", children: task?.name },
|
||||
{
|
||||
key: "dataset",
|
||||
label: "源数据集",
|
||||
children: (
|
||||
<Button
|
||||
style={{ paddingLeft: 0, marginLeft: 0 }}
|
||||
type="link"
|
||||
size="small"
|
||||
onClick={() =>
|
||||
navigate("/data/management/detail/" + task?.srcDatasetId)
|
||||
}
|
||||
>
|
||||
{task?.srcDatasetName}
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "targetDataset",
|
||||
label: "目标数据集",
|
||||
children: (
|
||||
<Button
|
||||
style={{ paddingLeft: 0, marginLeft: 0 }}
|
||||
type="link"
|
||||
size="small"
|
||||
onClick={() =>
|
||||
navigate("/data/management/detail/" + task?.destDatasetId)
|
||||
}
|
||||
>
|
||||
{task?.destDatasetName}
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
{ key: "startTime", label: "开始时间", children: task?.startedAt },
|
||||
{
|
||||
key: "description",
|
||||
label: "任务描述",
|
||||
children: (
|
||||
<span className="text-gray-600">{task?.description || "--"}</span>
|
||||
),
|
||||
span: 2,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* 执行摘要 */}
|
||||
<Card className="mb-6">
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
<div className="text-center p-4 bg-gradient-to-br from-blue-50 to-blue-100 rounded-lg">
|
||||
<Clock className="w-8 h-8 text-blue-500 mb-2 mx-auto" />
|
||||
<div className="text-xl font-bold text-blue-500">
|
||||
{formatExecutionDuration(task?.startedAt, task?.finishedAt) || "--"}
|
||||
</div>
|
||||
<div className="text-sm text-gray-600">总耗时</div>
|
||||
</div>
|
||||
<div className="text-center p-4 bg-gradient-to-br from-green-50 to-green-100 rounded-lg">
|
||||
<CheckCircle className="w-8 h-8 text-green-500 mb-2 mx-auto" />
|
||||
<div className="text-xl font-bold text-green-500">
|
||||
{task?.progress?.succeedFileNum || "0"}
|
||||
</div>
|
||||
<div className="text-sm text-gray-600">成功文件</div>
|
||||
</div>
|
||||
<div className="text-center p-4 bg-gradient-to-br from-red-50 to-red-100 rounded-lg">
|
||||
<AlertCircle className="w-8 h-8 text-red-500 mb-2 mx-auto" />
|
||||
<div className="text-xl font-bold text-red-500">
|
||||
{(task?.status.value === TaskStatus.RUNNING || task?.status.value === TaskStatus.PENDING) ?
|
||||
task?.progress.failedFileNum :
|
||||
task?.progress?.totalFileNum - task?.progress.succeedFileNum}
|
||||
</div>
|
||||
<div className="text-sm text-gray-600">失败文件</div>
|
||||
</div>
|
||||
<div className="text-center p-4 bg-gradient-to-br from-purple-50 to-purple-100 rounded-lg">
|
||||
<Activity className="w-8 h-8 text-purple-500 mb-2 mx-auto" />
|
||||
<div className="text-xl font-bold text-purple-500">
|
||||
{task?.progress?.successRate ? task?.progress?.successRate + "%" : "--"}
|
||||
</div>
|
||||
<div className="text-sm text-gray-600">成功率</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
{/* 基本信息 */}
|
||||
<Card>
|
||||
<div className="mb-8">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">基本信息</h3>
|
||||
<Descriptions
|
||||
column={2}
|
||||
bordered={false}
|
||||
size="middle"
|
||||
labelStyle={{ fontWeight: 500, color: "#555" }}
|
||||
contentStyle={{ fontSize: 14 }}
|
||||
items={descriptionItems}
|
||||
></Descriptions>
|
||||
</div>
|
||||
{/* 处理进度 */}
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">处理进度</h3>
|
||||
{ task?.status?.value === TaskStatus.FAILED ?
|
||||
<Progress percent={task?.progress?.process} size="small" status="exception" />
|
||||
: <Progress percent={task?.progress?.process} size="small"/>
|
||||
}
|
||||
<div className="grid grid-cols-2 gap-4 text-sm mt-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-3 h-3 bg-green-500 rounded-full inline-block" />
|
||||
<span>已完成: {task?.progress?.succeedFileNum || "0"}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-3 h-3 bg-blue-500 rounded-full inline-block" />
|
||||
<span>处理中: {(task?.status.value === TaskStatus.RUNNING || task?.status.value === TaskStatus.PENDING) ?
|
||||
task?.progress?.totalFileNum - task?.progress.succeedFileNum : 0}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-3 h-3 bg-red-500 rounded-full inline-block" />
|
||||
<span>失败: {(task?.status.value === TaskStatus.RUNNING || task?.status.value === TaskStatus.PENDING) ?
|
||||
task?.progress.failedFileNum :
|
||||
task?.progress?.totalFileNum - task?.progress.succeedFileNum}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,397 +1,397 @@
|
||||
import {Button, Modal, Table, Badge, Input, Popover} from "antd";
|
||||
import { Download } from "lucide-react";
|
||||
import {useEffect, useState} from "react";
|
||||
import {useParams} from "react-router";
|
||||
import {TaskStatus} from "@/pages/DataCleansing/cleansing.model.ts";
|
||||
import {TaskStatusMap} from "@/pages/DataCleansing/cleansing.const.tsx";
|
||||
|
||||
// 模拟文件列表数据
|
||||
export default function FileTable({result, fetchTaskResult}) {
|
||||
const { id = "" } = useParams();
|
||||
const [showFileCompareDialog, setShowFileCompareDialog] = useState(false);
|
||||
const [selectedFile, setSelectedFile] = useState<any>(null);
|
||||
const [selectedFileIds, setSelectedFileIds] = useState<string[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchTaskResult();
|
||||
}, [id]);
|
||||
|
||||
const handleSelectAllFiles = (checked: boolean) => {
|
||||
if (checked) {
|
||||
setSelectedFileIds(result.map((file) => file.instanceId));
|
||||
} else {
|
||||
setSelectedFileIds([]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectFile = (fileId: string, checked: boolean) => {
|
||||
if (checked) {
|
||||
setSelectedFileIds([...selectedFileIds, fileId]);
|
||||
} else {
|
||||
setSelectedFileIds(selectedFileIds.filter((id) => id !== fileId));
|
||||
}
|
||||
};
|
||||
const handleViewFileCompare = (file: any) => {
|
||||
setSelectedFile(file);
|
||||
setShowFileCompareDialog(true);
|
||||
};
|
||||
const handleBatchDownload = () => {
|
||||
// 实际下载逻辑
|
||||
};
|
||||
|
||||
function formatFileSize(bytes: number, decimals: number = 2): string {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
|
||||
const k = 1024;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(decimals)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
const fileColumns = [
|
||||
{
|
||||
title: (
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={
|
||||
selectedFileIds.length === result?.length && result?.length > 0
|
||||
}
|
||||
onChange={(e) => handleSelectAllFiles(e.target.checked)}
|
||||
className="w-4 h-4"
|
||||
/>
|
||||
),
|
||||
dataIndex: "select",
|
||||
key: "select",
|
||||
width: 50,
|
||||
render: (_text: string, record: any) => (
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedFileIds.includes(record.id)}
|
||||
onChange={(e) => handleSelectFile(record.id, e.target.checked)}
|
||||
className="w-4 h-4"
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "文件名",
|
||||
dataIndex: "srcName",
|
||||
key: "srcName",
|
||||
width: 200,
|
||||
filterDropdown: ({
|
||||
setSelectedKeys,
|
||||
selectedKeys,
|
||||
confirm,
|
||||
clearFilters,
|
||||
}: any) => (
|
||||
<div className="p-4 w-64">
|
||||
<Input
|
||||
placeholder="搜索文件名"
|
||||
value={selectedKeys[0]}
|
||||
onChange={(e) =>
|
||||
setSelectedKeys(e.target.value ? [e.target.value] : [])
|
||||
}
|
||||
onPressEnter={() => confirm()}
|
||||
className="mb-2"
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<Button size="small" onClick={() => confirm()}>
|
||||
搜索
|
||||
</Button>
|
||||
<Button size="small" onClick={() => clearFilters()}>
|
||||
重置
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
onFilter: (value: string, record: any) =>
|
||||
record.srcName.toLowerCase().includes(value.toLowerCase()),
|
||||
render: (text: string) => (
|
||||
<span>{text?.replace(/\.[^/.]+$/, "")}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "清洗后文件名",
|
||||
dataIndex: "destName",
|
||||
key: "destName",
|
||||
width: 200,
|
||||
filterDropdown: ({
|
||||
setSelectedKeys,
|
||||
selectedKeys,
|
||||
confirm,
|
||||
clearFilters,
|
||||
}: any) => (
|
||||
<div className="p-4 w-64">
|
||||
<Input
|
||||
placeholder="搜索文件名"
|
||||
value={selectedKeys[0]}
|
||||
onChange={(e) =>
|
||||
setSelectedKeys(e.target.value ? [e.target.value] : [])
|
||||
}
|
||||
onPressEnter={() => confirm()}
|
||||
className="mb-2"
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<Button size="small" onClick={() => confirm()}>
|
||||
搜索
|
||||
</Button>
|
||||
<Button size="small" onClick={() => clearFilters()}>
|
||||
重置
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
onFilter: (value: string, record: any) =>
|
||||
record.destName.toLowerCase().includes(value.toLowerCase()),
|
||||
render: (text: string) => (
|
||||
<span>{text?.replace(/\.[^/.]+$/, "")}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "文件类型",
|
||||
dataIndex: "srcType",
|
||||
key: "srcType",
|
||||
filterDropdown: ({
|
||||
setSelectedKeys,
|
||||
selectedKeys,
|
||||
confirm,
|
||||
clearFilters,
|
||||
}: any) => (
|
||||
<div className="p-4 w-64">
|
||||
<Input
|
||||
placeholder="搜索文件类型"
|
||||
value={selectedKeys[0]}
|
||||
onChange={(e) =>
|
||||
setSelectedKeys(e.target.value ? [e.target.value] : [])
|
||||
}
|
||||
onPressEnter={() => confirm()}
|
||||
className="mb-2"
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<Button size="small" onClick={() => confirm()}>
|
||||
搜索
|
||||
</Button>
|
||||
<Button size="small" onClick={() => clearFilters()}>
|
||||
重置
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
onFilter: (value: string, record: any) =>
|
||||
record.srcType.toLowerCase().includes(value.toLowerCase()),
|
||||
render: (text: string) => (
|
||||
<span className="font-mono text-sm">{text}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "清洗后文件类型",
|
||||
dataIndex: "destType",
|
||||
key: "destType",
|
||||
filterDropdown: ({
|
||||
setSelectedKeys,
|
||||
selectedKeys,
|
||||
confirm,
|
||||
clearFilters,
|
||||
}: any) => (
|
||||
<div className="p-4 w-64">
|
||||
<Input
|
||||
placeholder="搜索文件类型"
|
||||
value={selectedKeys[0]}
|
||||
onChange={(e) =>
|
||||
setSelectedKeys(e.target.value ? [e.target.value] : [])
|
||||
}
|
||||
onPressEnter={() => confirm()}
|
||||
className="mb-2"
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<Button size="small" onClick={() => confirm()}>
|
||||
搜索
|
||||
</Button>
|
||||
<Button size="small" onClick={() => clearFilters()}>
|
||||
重置
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
onFilter: (value: string, record: any) =>
|
||||
record.destType.toLowerCase().includes(value.toLowerCase()),
|
||||
render: (text: string) => (
|
||||
<span className="font-mono text-sm">{text}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "清洗前大小",
|
||||
dataIndex: "srcSize",
|
||||
key: "srcSize",
|
||||
sorter: (a: any, b: any) => {
|
||||
const getSizeInBytes = (size: string) => {
|
||||
if (!size || size === "-") return 0;
|
||||
const num = Number.parseFloat(size);
|
||||
if (size.includes("GB")) return num * 1024 * 1024 * 1024;
|
||||
if (size.includes("MB")) return num * 1024 * 1024;
|
||||
if (size.includes("KB")) return num * 1024;
|
||||
return num;
|
||||
};
|
||||
return getSizeInBytes(a.originalSize) - getSizeInBytes(b.originalSize);
|
||||
},
|
||||
render: (number: number) => (
|
||||
<span className="font-mono text-sm">{formatFileSize(number)}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "清洗后大小",
|
||||
dataIndex: "destSize",
|
||||
key: "destSize",
|
||||
sorter: (a: any, b: any) => {
|
||||
const getSizeInBytes = (size: string) => {
|
||||
if (!size || size === "-") return 0;
|
||||
const num = Number.parseFloat(size);
|
||||
if (size.includes("GB")) return num * 1024 * 1024 * 1024;
|
||||
if (size.includes("MB")) return num * 1024 * 1024;
|
||||
if (size.includes("KB")) return num * 1024;
|
||||
return num;
|
||||
};
|
||||
return (
|
||||
getSizeInBytes(a.processedSize) - getSizeInBytes(b.processedSize)
|
||||
);
|
||||
},
|
||||
render: (number: number) => (
|
||||
<span className="font-mono text-sm">{formatFileSize(number)}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "状态",
|
||||
dataIndex: "status",
|
||||
key: "status",
|
||||
filters: [
|
||||
{ text: "已完成", value: "COMPLETED" },
|
||||
{ text: "失败", value: "FAILED" },
|
||||
],
|
||||
onFilter: (value: string, record: any) => record.status === value,
|
||||
render: (status: string) => (
|
||||
<Badge
|
||||
status={
|
||||
status === "COMPLETED"
|
||||
? "success"
|
||||
: "error"
|
||||
}
|
||||
text={TaskStatusMap[status as TaskStatus].label}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
key: "action",
|
||||
width: 200,
|
||||
render: (_text: string, record: any) => (
|
||||
<div className="flex">
|
||||
{record.status === "COMPLETED" ? (
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
onClick={() => handleViewFileCompare(record)}
|
||||
>
|
||||
对比
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
disabled
|
||||
>
|
||||
对比
|
||||
</Button>
|
||||
)}
|
||||
<Popover content="暂未开放">
|
||||
<Button type="link" size="small" disabled>下载</Button>
|
||||
</Popover>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
{selectedFileIds.length > 0 && (
|
||||
<div className="mb-4 flex justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-gray-600">
|
||||
已选择 {selectedFileIds.length} 个文件
|
||||
</span>
|
||||
<Button
|
||||
onClick={handleBatchDownload}
|
||||
size="small"
|
||||
type="primary"
|
||||
icon={<Download className="w-4 h-4 mr-2" />}
|
||||
>
|
||||
批量下载
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<Table
|
||||
columns={fileColumns}
|
||||
dataSource={result}
|
||||
pagination={{ pageSize: 10, showSizeChanger: true }}
|
||||
size="middle"
|
||||
rowKey="id"
|
||||
/>
|
||||
|
||||
{/* 文件对比弹窗 */}
|
||||
<Modal
|
||||
open={showFileCompareDialog}
|
||||
onCancel={() => setShowFileCompareDialog(false)}
|
||||
footer={null}
|
||||
width={900}
|
||||
title={<span>文件对比 - {selectedFile?.fileName}</span>}
|
||||
>
|
||||
<div className="grid grid-cols-2 gap-6 py-6">
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900">清洗前</h4>
|
||||
<div className="border border-gray-200 rounded-lg p-6 bg-gray-50 min-h-48 flex items-center justify-center">
|
||||
<div className="text-center text-gray-500">
|
||||
<div className="w-16 h-16 bg-gray-300 rounded-lg mx-auto mb-2" />
|
||||
<div className="text-sm">原始文件预览</div>
|
||||
<div className="text-xs text-gray-400">
|
||||
大小: {formatFileSize(selectedFile?.srcSize)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-sm text-gray-600 mt-3 space-y-1">
|
||||
<div>
|
||||
<span className="font-medium">文件格式:</span> {selectedFile?.srcType}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900">清洗后</h4>
|
||||
<div className="border border-gray-200 rounded-lg p-6 bg-gray-50 min-h-48 flex items-center justify-center">
|
||||
<div className="text-center text-gray-500">
|
||||
<div className="w-16 h-16 bg-blue-300 rounded-lg mx-auto mb-2" />
|
||||
<div className="text-sm">处理后文件预览</div>
|
||||
<div className="text-xs text-gray-400">
|
||||
大小: {formatFileSize(selectedFile?.destSize)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-sm text-gray-600 mt-3 space-y-1">
|
||||
<div>
|
||||
<span className="font-medium">文件格式:</span> {selectedFile?.destType}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="border-t border-gray-200 mt-6 pt-4">
|
||||
<h4 className="font-medium text-gray-900 mb-3">处理效果对比</h4>
|
||||
<div className="grid grid-cols-3 gap-4 text-sm">
|
||||
<div className="bg-green-50 p-4 rounded-lg">
|
||||
<div className="font-medium text-green-700">文件大小优化</div>
|
||||
<div className="text-green-600">减少了 {(100 * (selectedFile?.srcSize - selectedFile?.destSize) / selectedFile?.srcSize).toFixed(2)}%</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
import {Button, Modal, Table, Badge, Input, Popover} from "antd";
|
||||
import { Download } from "lucide-react";
|
||||
import {useEffect, useState} from "react";
|
||||
import {useParams} from "react-router";
|
||||
import {TaskStatus} from "@/pages/DataCleansing/cleansing.model.ts";
|
||||
import {TaskStatusMap} from "@/pages/DataCleansing/cleansing.const.tsx";
|
||||
|
||||
// 模拟文件列表数据
|
||||
export default function FileTable({result, fetchTaskResult}) {
|
||||
const { id = "" } = useParams();
|
||||
const [showFileCompareDialog, setShowFileCompareDialog] = useState(false);
|
||||
const [selectedFile, setSelectedFile] = useState<any>(null);
|
||||
const [selectedFileIds, setSelectedFileIds] = useState<string[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchTaskResult();
|
||||
}, [id]);
|
||||
|
||||
const handleSelectAllFiles = (checked: boolean) => {
|
||||
if (checked) {
|
||||
setSelectedFileIds(result.map((file) => file.instanceId));
|
||||
} else {
|
||||
setSelectedFileIds([]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectFile = (fileId: string, checked: boolean) => {
|
||||
if (checked) {
|
||||
setSelectedFileIds([...selectedFileIds, fileId]);
|
||||
} else {
|
||||
setSelectedFileIds(selectedFileIds.filter((id) => id !== fileId));
|
||||
}
|
||||
};
|
||||
const handleViewFileCompare = (file: any) => {
|
||||
setSelectedFile(file);
|
||||
setShowFileCompareDialog(true);
|
||||
};
|
||||
const handleBatchDownload = () => {
|
||||
// 实际下载逻辑
|
||||
};
|
||||
|
||||
function formatFileSize(bytes: number, decimals: number = 2): string {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
|
||||
const k = 1024;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(decimals)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
const fileColumns = [
|
||||
{
|
||||
title: (
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={
|
||||
selectedFileIds.length === result?.length && result?.length > 0
|
||||
}
|
||||
onChange={(e) => handleSelectAllFiles(e.target.checked)}
|
||||
className="w-4 h-4"
|
||||
/>
|
||||
),
|
||||
dataIndex: "select",
|
||||
key: "select",
|
||||
width: 50,
|
||||
render: (_text: string, record: any) => (
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedFileIds.includes(record.id)}
|
||||
onChange={(e) => handleSelectFile(record.id, e.target.checked)}
|
||||
className="w-4 h-4"
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "文件名",
|
||||
dataIndex: "srcName",
|
||||
key: "srcName",
|
||||
width: 200,
|
||||
filterDropdown: ({
|
||||
setSelectedKeys,
|
||||
selectedKeys,
|
||||
confirm,
|
||||
clearFilters,
|
||||
}: any) => (
|
||||
<div className="p-4 w-64">
|
||||
<Input
|
||||
placeholder="搜索文件名"
|
||||
value={selectedKeys[0]}
|
||||
onChange={(e) =>
|
||||
setSelectedKeys(e.target.value ? [e.target.value] : [])
|
||||
}
|
||||
onPressEnter={() => confirm()}
|
||||
className="mb-2"
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<Button size="small" onClick={() => confirm()}>
|
||||
搜索
|
||||
</Button>
|
||||
<Button size="small" onClick={() => clearFilters()}>
|
||||
重置
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
onFilter: (value: string, record: any) =>
|
||||
record.srcName.toLowerCase().includes(value.toLowerCase()),
|
||||
render: (text: string) => (
|
||||
<span>{text?.replace(/\.[^/.]+$/, "")}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "清洗后文件名",
|
||||
dataIndex: "destName",
|
||||
key: "destName",
|
||||
width: 200,
|
||||
filterDropdown: ({
|
||||
setSelectedKeys,
|
||||
selectedKeys,
|
||||
confirm,
|
||||
clearFilters,
|
||||
}: any) => (
|
||||
<div className="p-4 w-64">
|
||||
<Input
|
||||
placeholder="搜索文件名"
|
||||
value={selectedKeys[0]}
|
||||
onChange={(e) =>
|
||||
setSelectedKeys(e.target.value ? [e.target.value] : [])
|
||||
}
|
||||
onPressEnter={() => confirm()}
|
||||
className="mb-2"
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<Button size="small" onClick={() => confirm()}>
|
||||
搜索
|
||||
</Button>
|
||||
<Button size="small" onClick={() => clearFilters()}>
|
||||
重置
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
onFilter: (value: string, record: any) =>
|
||||
record.destName.toLowerCase().includes(value.toLowerCase()),
|
||||
render: (text: string) => (
|
||||
<span>{text?.replace(/\.[^/.]+$/, "")}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "文件类型",
|
||||
dataIndex: "srcType",
|
||||
key: "srcType",
|
||||
filterDropdown: ({
|
||||
setSelectedKeys,
|
||||
selectedKeys,
|
||||
confirm,
|
||||
clearFilters,
|
||||
}: any) => (
|
||||
<div className="p-4 w-64">
|
||||
<Input
|
||||
placeholder="搜索文件类型"
|
||||
value={selectedKeys[0]}
|
||||
onChange={(e) =>
|
||||
setSelectedKeys(e.target.value ? [e.target.value] : [])
|
||||
}
|
||||
onPressEnter={() => confirm()}
|
||||
className="mb-2"
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<Button size="small" onClick={() => confirm()}>
|
||||
搜索
|
||||
</Button>
|
||||
<Button size="small" onClick={() => clearFilters()}>
|
||||
重置
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
onFilter: (value: string, record: any) =>
|
||||
record.srcType.toLowerCase().includes(value.toLowerCase()),
|
||||
render: (text: string) => (
|
||||
<span className="font-mono text-sm">{text}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "清洗后文件类型",
|
||||
dataIndex: "destType",
|
||||
key: "destType",
|
||||
filterDropdown: ({
|
||||
setSelectedKeys,
|
||||
selectedKeys,
|
||||
confirm,
|
||||
clearFilters,
|
||||
}: any) => (
|
||||
<div className="p-4 w-64">
|
||||
<Input
|
||||
placeholder="搜索文件类型"
|
||||
value={selectedKeys[0]}
|
||||
onChange={(e) =>
|
||||
setSelectedKeys(e.target.value ? [e.target.value] : [])
|
||||
}
|
||||
onPressEnter={() => confirm()}
|
||||
className="mb-2"
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<Button size="small" onClick={() => confirm()}>
|
||||
搜索
|
||||
</Button>
|
||||
<Button size="small" onClick={() => clearFilters()}>
|
||||
重置
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
onFilter: (value: string, record: any) =>
|
||||
record.destType.toLowerCase().includes(value.toLowerCase()),
|
||||
render: (text: string) => (
|
||||
<span className="font-mono text-sm">{text}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "清洗前大小",
|
||||
dataIndex: "srcSize",
|
||||
key: "srcSize",
|
||||
sorter: (a: any, b: any) => {
|
||||
const getSizeInBytes = (size: string) => {
|
||||
if (!size || size === "-") return 0;
|
||||
const num = Number.parseFloat(size);
|
||||
if (size.includes("GB")) return num * 1024 * 1024 * 1024;
|
||||
if (size.includes("MB")) return num * 1024 * 1024;
|
||||
if (size.includes("KB")) return num * 1024;
|
||||
return num;
|
||||
};
|
||||
return getSizeInBytes(a.originalSize) - getSizeInBytes(b.originalSize);
|
||||
},
|
||||
render: (number: number) => (
|
||||
<span className="font-mono text-sm">{formatFileSize(number)}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "清洗后大小",
|
||||
dataIndex: "destSize",
|
||||
key: "destSize",
|
||||
sorter: (a: any, b: any) => {
|
||||
const getSizeInBytes = (size: string) => {
|
||||
if (!size || size === "-") return 0;
|
||||
const num = Number.parseFloat(size);
|
||||
if (size.includes("GB")) return num * 1024 * 1024 * 1024;
|
||||
if (size.includes("MB")) return num * 1024 * 1024;
|
||||
if (size.includes("KB")) return num * 1024;
|
||||
return num;
|
||||
};
|
||||
return (
|
||||
getSizeInBytes(a.processedSize) - getSizeInBytes(b.processedSize)
|
||||
);
|
||||
},
|
||||
render: (number: number) => (
|
||||
<span className="font-mono text-sm">{formatFileSize(number)}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "状态",
|
||||
dataIndex: "status",
|
||||
key: "status",
|
||||
filters: [
|
||||
{ text: "已完成", value: "COMPLETED" },
|
||||
{ text: "失败", value: "FAILED" },
|
||||
],
|
||||
onFilter: (value: string, record: any) => record.status === value,
|
||||
render: (status: string) => (
|
||||
<Badge
|
||||
status={
|
||||
status === "COMPLETED"
|
||||
? "success"
|
||||
: "error"
|
||||
}
|
||||
text={TaskStatusMap[status as TaskStatus].label}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
key: "action",
|
||||
width: 200,
|
||||
render: (_text: string, record: any) => (
|
||||
<div className="flex">
|
||||
{record.status === "COMPLETED" ? (
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
onClick={() => handleViewFileCompare(record)}
|
||||
>
|
||||
对比
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
disabled
|
||||
>
|
||||
对比
|
||||
</Button>
|
||||
)}
|
||||
<Popover content="暂未开放">
|
||||
<Button type="link" size="small" disabled>下载</Button>
|
||||
</Popover>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
{selectedFileIds.length > 0 && (
|
||||
<div className="mb-4 flex justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-gray-600">
|
||||
已选择 {selectedFileIds.length} 个文件
|
||||
</span>
|
||||
<Button
|
||||
onClick={handleBatchDownload}
|
||||
size="small"
|
||||
type="primary"
|
||||
icon={<Download className="w-4 h-4 mr-2" />}
|
||||
>
|
||||
批量下载
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<Table
|
||||
columns={fileColumns}
|
||||
dataSource={result}
|
||||
pagination={{ pageSize: 10, showSizeChanger: true }}
|
||||
size="middle"
|
||||
rowKey="id"
|
||||
/>
|
||||
|
||||
{/* 文件对比弹窗 */}
|
||||
<Modal
|
||||
open={showFileCompareDialog}
|
||||
onCancel={() => setShowFileCompareDialog(false)}
|
||||
footer={null}
|
||||
width={900}
|
||||
title={<span>文件对比 - {selectedFile?.fileName}</span>}
|
||||
>
|
||||
<div className="grid grid-cols-2 gap-6 py-6">
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900">清洗前</h4>
|
||||
<div className="border border-gray-200 rounded-lg p-6 bg-gray-50 min-h-48 flex items-center justify-center">
|
||||
<div className="text-center text-gray-500">
|
||||
<div className="w-16 h-16 bg-gray-300 rounded-lg mx-auto mb-2" />
|
||||
<div className="text-sm">原始文件预览</div>
|
||||
<div className="text-xs text-gray-400">
|
||||
大小: {formatFileSize(selectedFile?.srcSize)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-sm text-gray-600 mt-3 space-y-1">
|
||||
<div>
|
||||
<span className="font-medium">文件格式:</span> {selectedFile?.srcType}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900">清洗后</h4>
|
||||
<div className="border border-gray-200 rounded-lg p-6 bg-gray-50 min-h-48 flex items-center justify-center">
|
||||
<div className="text-center text-gray-500">
|
||||
<div className="w-16 h-16 bg-blue-300 rounded-lg mx-auto mb-2" />
|
||||
<div className="text-sm">处理后文件预览</div>
|
||||
<div className="text-xs text-gray-400">
|
||||
大小: {formatFileSize(selectedFile?.destSize)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-sm text-gray-600 mt-3 space-y-1">
|
||||
<div>
|
||||
<span className="font-medium">文件格式:</span> {selectedFile?.destType}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="border-t border-gray-200 mt-6 pt-4">
|
||||
<h4 className="font-medium text-gray-900 mb-3">处理效果对比</h4>
|
||||
<div className="grid grid-cols-3 gap-4 text-sm">
|
||||
<div className="bg-green-50 p-4 rounded-lg">
|
||||
<div className="font-medium text-green-700">文件大小优化</div>
|
||||
<div className="text-green-600">减少了 {(100 * (selectedFile?.srcSize - selectedFile?.destSize) / selectedFile?.srcSize).toFixed(2)}%</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,43 +1,43 @@
|
||||
import {useEffect} from "react";
|
||||
import {useParams} from "react-router";
|
||||
import {FileClock} from "lucide-react";
|
||||
|
||||
export default function LogsTable({taskLog, fetchTaskLog} : {taskLog: any[], fetchTaskLog: () => Promise<any>}) {
|
||||
const { id = "" } = useParams();
|
||||
|
||||
useEffect(() => {
|
||||
fetchTaskLog();
|
||||
}, [id]);
|
||||
|
||||
return taskLog?.length > 0 ? (
|
||||
<>
|
||||
<div className="text-gray-300 p-4 border border-gray-700 bg-gray-800 rounded-lg">
|
||||
<div className="font-mono text-sm">
|
||||
{taskLog?.map?.((log, index) => (
|
||||
<div key={index} className="flex gap-3">
|
||||
<span
|
||||
className={`min-w-20 ${
|
||||
log.level === "ERROR" || log.level === "FATAL"
|
||||
? "text-red-500"
|
||||
: log.level === "WARNING" || log.level === "WARN"
|
||||
? "text-yellow-500"
|
||||
: "text-green-500"
|
||||
}`}
|
||||
>
|
||||
[{log.level}]
|
||||
</span>
|
||||
<span className="text-gray-100">{log.message}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="text-center py-12">
|
||||
<FileClock className="w-16 h-16 text-gray-300 mx-auto mb-4" />
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">
|
||||
当前任务无可用日志
|
||||
</h3>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
import {useEffect} from "react";
|
||||
import {useParams} from "react-router";
|
||||
import {FileClock} from "lucide-react";
|
||||
|
||||
export default function LogsTable({taskLog, fetchTaskLog} : {taskLog: any[], fetchTaskLog: () => Promise<any>}) {
|
||||
const { id = "" } = useParams();
|
||||
|
||||
useEffect(() => {
|
||||
fetchTaskLog();
|
||||
}, [id]);
|
||||
|
||||
return taskLog?.length > 0 ? (
|
||||
<>
|
||||
<div className="text-gray-300 p-4 border border-gray-700 bg-gray-800 rounded-lg">
|
||||
<div className="font-mono text-sm">
|
||||
{taskLog?.map?.((log, index) => (
|
||||
<div key={index} className="flex gap-3">
|
||||
<span
|
||||
className={`min-w-20 ${
|
||||
log.level === "ERROR" || log.level === "FATAL"
|
||||
? "text-red-500"
|
||||
: log.level === "WARNING" || log.level === "WARN"
|
||||
? "text-yellow-500"
|
||||
: "text-green-500"
|
||||
}`}
|
||||
>
|
||||
[{log.level}]
|
||||
</span>
|
||||
<span className="text-gray-100">{log.message}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="text-center py-12">
|
||||
<FileClock className="w-16 h-16 text-gray-300 mx-auto mb-4" />
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">
|
||||
当前任务无可用日志
|
||||
</h3>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
import {Steps, Typography} from "antd";
|
||||
import {useNavigate} from "react-router";
|
||||
|
||||
export default function OperatorTable({ task }: { task: any }) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
return task?.instance?.length > 0 && (
|
||||
<>
|
||||
<Steps
|
||||
progressDot
|
||||
direction="vertical"
|
||||
items={Object.values(task?.instance).map((item) => ({
|
||||
title: <Typography.Link
|
||||
onClick={() => navigate(`/data/operator-market/plugin-detail/${item?.id}`)}
|
||||
>
|
||||
{item?.name}
|
||||
</Typography.Link>,
|
||||
description: item?.description,
|
||||
status: "finish"
|
||||
}))}
|
||||
className="overflow-auto"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
import {Steps, Typography} from "antd";
|
||||
import {useNavigate} from "react-router";
|
||||
|
||||
export default function OperatorTable({ task }: { task: any }) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
return task?.instance?.length > 0 && (
|
||||
<>
|
||||
<Steps
|
||||
progressDot
|
||||
direction="vertical"
|
||||
items={Object.values(task?.instance).map((item) => ({
|
||||
title: <Typography.Link
|
||||
onClick={() => navigate(`/data/operator-market/plugin-detail/${item?.id}`)}
|
||||
>
|
||||
{item?.name}
|
||||
</Typography.Link>,
|
||||
description: item?.description,
|
||||
status: "finish"
|
||||
}))}
|
||||
className="overflow-auto"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,61 +1,61 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Tabs, Button } from "antd";
|
||||
import { PlusOutlined } from "@ant-design/icons";
|
||||
import { useNavigate } from "react-router";
|
||||
import TaskList from "./components/TaskList";
|
||||
import TemplateList from "./components/TemplateList";
|
||||
import ProcessFlowDiagram from "./components/ProcessFlowDiagram";
|
||||
import { useSearchParams } from "@/hooks/useSearchParams";
|
||||
|
||||
export default function DataProcessingPage() {
|
||||
const navigate = useNavigate();
|
||||
const urlParams = useSearchParams();
|
||||
const [currentView, setCurrentView] = useState<"task" | "template">("task");
|
||||
|
||||
useEffect(() => {
|
||||
if (urlParams.view) {
|
||||
setCurrentView(urlParams.view);
|
||||
}
|
||||
}, [urlParams]);
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col gap-4">
|
||||
{/* Header */}
|
||||
<div className="flex justify-between items-center">
|
||||
<h1 className="text-xl font-bold">数据清洗</h1>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => navigate("/data/cleansing/create-template")}
|
||||
>
|
||||
创建清洗模板
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => navigate("/data/cleansing/create-task")}
|
||||
>
|
||||
创建清洗任务
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<ProcessFlowDiagram />
|
||||
<Tabs
|
||||
activeKey={currentView}
|
||||
onChange={(key) => setCurrentView(key as any)}
|
||||
items={[
|
||||
{
|
||||
key: "task",
|
||||
label: "任务列表",
|
||||
},
|
||||
{
|
||||
key: "template",
|
||||
label: "模板管理",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
{currentView === "task" && <TaskList />}
|
||||
{currentView === "template" && <TemplateList />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
import { useEffect, useState } from "react";
|
||||
import { Tabs, Button } from "antd";
|
||||
import { PlusOutlined } from "@ant-design/icons";
|
||||
import { useNavigate } from "react-router";
|
||||
import TaskList from "./components/TaskList";
|
||||
import TemplateList from "./components/TemplateList";
|
||||
import ProcessFlowDiagram from "./components/ProcessFlowDiagram";
|
||||
import { useSearchParams } from "@/hooks/useSearchParams";
|
||||
|
||||
export default function DataProcessingPage() {
|
||||
const navigate = useNavigate();
|
||||
const urlParams = useSearchParams();
|
||||
const [currentView, setCurrentView] = useState<"task" | "template">("task");
|
||||
|
||||
useEffect(() => {
|
||||
if (urlParams.view) {
|
||||
setCurrentView(urlParams.view);
|
||||
}
|
||||
}, [urlParams]);
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col gap-4">
|
||||
{/* Header */}
|
||||
<div className="flex justify-between items-center">
|
||||
<h1 className="text-xl font-bold">数据清洗</h1>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => navigate("/data/cleansing/create-template")}
|
||||
>
|
||||
创建清洗模板
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => navigate("/data/cleansing/create-task")}
|
||||
>
|
||||
创建清洗任务
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<ProcessFlowDiagram />
|
||||
<Tabs
|
||||
activeKey={currentView}
|
||||
onChange={(key) => setCurrentView(key as any)}
|
||||
items={[
|
||||
{
|
||||
key: "task",
|
||||
label: "任务列表",
|
||||
},
|
||||
{
|
||||
key: "template",
|
||||
label: "模板管理",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
{currentView === "task" && <TaskList />}
|
||||
{currentView === "template" && <TemplateList />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,86 +1,86 @@
|
||||
import {
|
||||
ArrowRight,
|
||||
CheckCircle,
|
||||
Database,
|
||||
Play,
|
||||
Settings,
|
||||
Workflow,
|
||||
Zap,
|
||||
} from "lucide-react";
|
||||
|
||||
// 流程图组件
|
||||
export default function ProcessFlowDiagram() {
|
||||
const flowSteps = [
|
||||
{
|
||||
id: "start",
|
||||
label: "开始",
|
||||
type: "start",
|
||||
icon: Play,
|
||||
color: "bg-green-500",
|
||||
},
|
||||
{
|
||||
id: "select",
|
||||
label: "选择数据集",
|
||||
type: "process",
|
||||
icon: Database,
|
||||
color: "bg-blue-500",
|
||||
},
|
||||
{
|
||||
id: "config",
|
||||
label: "基本配置",
|
||||
type: "process",
|
||||
icon: Settings,
|
||||
color: "bg-purple-500",
|
||||
},
|
||||
{
|
||||
id: "operators",
|
||||
label: "算子编排",
|
||||
type: "process",
|
||||
icon: Workflow,
|
||||
color: "bg-orange-500",
|
||||
},
|
||||
{
|
||||
id: "execute",
|
||||
label: "执行任务",
|
||||
type: "process",
|
||||
icon: Zap,
|
||||
color: "bg-red-500",
|
||||
},
|
||||
{
|
||||
id: "end",
|
||||
label: "完成",
|
||||
type: "end",
|
||||
icon: CheckCircle,
|
||||
color: "bg-green-500",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="border-card p-6">
|
||||
<div className="w-full flex items-center justify-center">
|
||||
<div className="w-full flex items-center space-x-12">
|
||||
{flowSteps.map((step, index) => {
|
||||
const IconComponent = step.icon;
|
||||
return (
|
||||
<div key={step.id} className="flex-1 flex items-center">
|
||||
<div className="flex flex-col items-center w-full">
|
||||
<div
|
||||
className={`w-12 h-12 ${step.color} rounded-full flex items-center justify-center text-white shadow-lg`}
|
||||
>
|
||||
<IconComponent className="w-6 h-6" />
|
||||
</div>
|
||||
<span className="text-xs font-medium text-gray-700 mt-2 text-center max-w-16">
|
||||
{step.label}
|
||||
</span>
|
||||
</div>
|
||||
{index < flowSteps.length - 1 && (
|
||||
<ArrowRight className="w-6 h-6 text-gray-400 mx-3" />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
import {
|
||||
ArrowRight,
|
||||
CheckCircle,
|
||||
Database,
|
||||
Play,
|
||||
Settings,
|
||||
Workflow,
|
||||
Zap,
|
||||
} from "lucide-react";
|
||||
|
||||
// 流程图组件
|
||||
export default function ProcessFlowDiagram() {
|
||||
const flowSteps = [
|
||||
{
|
||||
id: "start",
|
||||
label: "开始",
|
||||
type: "start",
|
||||
icon: Play,
|
||||
color: "bg-green-500",
|
||||
},
|
||||
{
|
||||
id: "select",
|
||||
label: "选择数据集",
|
||||
type: "process",
|
||||
icon: Database,
|
||||
color: "bg-blue-500",
|
||||
},
|
||||
{
|
||||
id: "config",
|
||||
label: "基本配置",
|
||||
type: "process",
|
||||
icon: Settings,
|
||||
color: "bg-purple-500",
|
||||
},
|
||||
{
|
||||
id: "operators",
|
||||
label: "算子编排",
|
||||
type: "process",
|
||||
icon: Workflow,
|
||||
color: "bg-orange-500",
|
||||
},
|
||||
{
|
||||
id: "execute",
|
||||
label: "执行任务",
|
||||
type: "process",
|
||||
icon: Zap,
|
||||
color: "bg-red-500",
|
||||
},
|
||||
{
|
||||
id: "end",
|
||||
label: "完成",
|
||||
type: "end",
|
||||
icon: CheckCircle,
|
||||
color: "bg-green-500",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="border-card p-6">
|
||||
<div className="w-full flex items-center justify-center">
|
||||
<div className="w-full flex items-center space-x-12">
|
||||
{flowSteps.map((step, index) => {
|
||||
const IconComponent = step.icon;
|
||||
return (
|
||||
<div key={step.id} className="flex-1 flex items-center">
|
||||
<div className="flex flex-col items-center w-full">
|
||||
<div
|
||||
className={`w-12 h-12 ${step.color} rounded-full flex items-center justify-center text-white shadow-lg`}
|
||||
>
|
||||
<IconComponent className="w-6 h-6" />
|
||||
</div>
|
||||
<span className="text-xs font-medium text-gray-700 mt-2 text-center max-w-16">
|
||||
{step.label}
|
||||
</span>
|
||||
</div>
|
||||
{index < flowSteps.length - 1 && (
|
||||
<ArrowRight className="w-6 h-6 text-gray-400 mx-3" />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,308 +1,308 @@
|
||||
import { useState } from "react";
|
||||
import { Table, Progress, Badge, Button, Tooltip, Card, App } from "antd";
|
||||
import {
|
||||
PlayCircleOutlined,
|
||||
PauseCircleOutlined,
|
||||
DeleteOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { SearchControls } from "@/components/SearchControls";
|
||||
import CardView from "@/components/CardView";
|
||||
import { useNavigate } from "react-router";
|
||||
import { mapTask, TaskStatusMap } from "../../cleansing.const";
|
||||
import {
|
||||
TaskStatus,
|
||||
type CleansingTask,
|
||||
} from "@/pages/DataCleansing/cleansing.model";
|
||||
import useFetchData from "@/hooks/useFetchData";
|
||||
import {
|
||||
deleteCleaningTaskByIdUsingDelete,
|
||||
executeCleaningTaskUsingPost,
|
||||
queryCleaningTasksUsingGet,
|
||||
stopCleaningTaskUsingPost,
|
||||
} from "../../cleansing.api";
|
||||
|
||||
export default function TaskList() {
|
||||
const navigate = useNavigate();
|
||||
const { message } = App.useApp();
|
||||
const [viewMode, setViewMode] = useState<"card" | "list">("list");
|
||||
const filterOptions = [
|
||||
{
|
||||
key: "status",
|
||||
label: "状态",
|
||||
options: [...Object.values(TaskStatusMap)],
|
||||
},
|
||||
];
|
||||
|
||||
const {
|
||||
loading,
|
||||
tableData,
|
||||
pagination,
|
||||
searchParams,
|
||||
setSearchParams,
|
||||
fetchData,
|
||||
handleFiltersChange,
|
||||
handleKeywordChange,
|
||||
} = useFetchData(queryCleaningTasksUsingGet, mapTask);
|
||||
|
||||
const pauseTask = async (item: CleansingTask) => {
|
||||
await stopCleaningTaskUsingPost(item.id);
|
||||
message.success("任务已暂停");
|
||||
fetchData();
|
||||
};
|
||||
|
||||
const startTask = async (item: CleansingTask) => {
|
||||
await executeCleaningTaskUsingPost(item.id);
|
||||
message.success("任务已启动");
|
||||
fetchData();
|
||||
};
|
||||
|
||||
const deleteTask = async (item: CleansingTask) => {
|
||||
await deleteCleaningTaskByIdUsingDelete(item.id);
|
||||
message.success("任务已删除");
|
||||
fetchData();
|
||||
};
|
||||
|
||||
const taskOperations = (record: CleansingTask) => {
|
||||
const isRunning = record.status?.value === TaskStatus.RUNNING;
|
||||
const showStart = [
|
||||
TaskStatus.PENDING,
|
||||
TaskStatus.FAILED,
|
||||
TaskStatus.STOPPED,
|
||||
].includes(record.status?.value);
|
||||
const pauseBtn = {
|
||||
key: "pause",
|
||||
label: "暂停",
|
||||
icon: isRunning ? <PauseCircleOutlined /> : <PlayCircleOutlined />,
|
||||
onClick: pauseTask, // implement pause/play logic
|
||||
};
|
||||
|
||||
const startBtn = {
|
||||
key: "start",
|
||||
label: "启动",
|
||||
icon: isRunning ? <PauseCircleOutlined /> : <PlayCircleOutlined />,
|
||||
onClick: startTask, // implement pause/play logic
|
||||
};
|
||||
return [
|
||||
...(isRunning
|
||||
? [ pauseBtn ]
|
||||
: []),
|
||||
...(showStart
|
||||
? [ startBtn ]
|
||||
: []),
|
||||
{
|
||||
key: "delete",
|
||||
label: "删除",
|
||||
danger: true,
|
||||
icon: <DeleteOutlined />,
|
||||
onClick: deleteTask, // implement delete logic
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
const taskColumns = [
|
||||
{
|
||||
title: "任务名称",
|
||||
dataIndex: "name",
|
||||
key: "name",
|
||||
fixed: "left",
|
||||
width: 150,
|
||||
ellipsis: true,
|
||||
render: (_, task: CleansingTask) => {
|
||||
return (
|
||||
<Button
|
||||
type="link"
|
||||
onClick={() =>
|
||||
navigate("/data/cleansing/task-detail/" + task.id)
|
||||
}
|
||||
>
|
||||
{task.name}
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "任务ID",
|
||||
dataIndex: "id",
|
||||
key: "id",
|
||||
width: 150,
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: "源数据集",
|
||||
dataIndex: "srcDatasetId",
|
||||
key: "srcDatasetId",
|
||||
width: 150,
|
||||
ellipsis: true,
|
||||
render: (_, record: CleansingTask) => {
|
||||
return (
|
||||
<Button
|
||||
type="link"
|
||||
onClick={() =>
|
||||
navigate("/data/management/detail/" + record.srcDatasetId)
|
||||
}
|
||||
>
|
||||
{record.srcDatasetName}
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "目标数据集",
|
||||
dataIndex: "destDatasetId",
|
||||
key: "destDatasetId",
|
||||
width: 150,
|
||||
ellipsis: true,
|
||||
render: (_, record: CleansingTask) => {
|
||||
return (
|
||||
<Button
|
||||
type="link"
|
||||
onClick={() =>
|
||||
navigate("/data/management/detail/" + record.destDatasetId)
|
||||
}
|
||||
>
|
||||
{record.destDatasetName}
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "状态",
|
||||
dataIndex: "status",
|
||||
key: "status",
|
||||
width: 100,
|
||||
render: (status: any) => {
|
||||
return <Badge color={status?.color} text={status?.label} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "进度",
|
||||
dataIndex: "process",
|
||||
key: "process",
|
||||
width: 150,
|
||||
render: (_, record: CleansingTask) => {
|
||||
if (record?.status?.value == TaskStatus.FAILED) {
|
||||
return <Progress percent={record?.progress?.process} size="small" status="exception" />;
|
||||
}
|
||||
return <Progress percent={record?.progress?.process} size="small"/>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "已处理文件数",
|
||||
dataIndex: "finishedFileNum",
|
||||
key: "finishedFileNum",
|
||||
width: 120,
|
||||
align: "right",
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: "总文件数",
|
||||
dataIndex: "totalFileNum",
|
||||
key: "totalFileNum",
|
||||
width: 100,
|
||||
align: "right",
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: "执行耗时",
|
||||
dataIndex: "duration",
|
||||
key: "duration",
|
||||
width: 100,
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: "开始时间",
|
||||
dataIndex: "startedAt",
|
||||
key: "startedAt",
|
||||
width: 180,
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: "结束时间",
|
||||
dataIndex: "finishedAt",
|
||||
key: "finishedAt",
|
||||
width: 180,
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: "创建时间",
|
||||
dataIndex: "createdAt",
|
||||
key: "createdAt",
|
||||
width: 180,
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: "数据量变化",
|
||||
dataIndex: "dataSizeChange",
|
||||
key: "dataSizeChange",
|
||||
width: 180,
|
||||
ellipsis: true,
|
||||
render: (_: any, record: CleansingTask) => {
|
||||
if (record.before !== undefined && record.after !== undefined) {
|
||||
return `${record.before} → ${record.after}`;
|
||||
}
|
||||
return "-";
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
key: "action",
|
||||
fixed: "right",
|
||||
render: (text: string, record: any) => (
|
||||
<div className="flex gap-2">
|
||||
{taskOperations(record).map((op) =>
|
||||
op ? (
|
||||
<Tooltip key={op.key} title={op.label}>
|
||||
<Button
|
||||
type="text"
|
||||
icon={op.icon}
|
||||
danger={op?.danger}
|
||||
onClick={() => op.onClick(record)}
|
||||
/>
|
||||
</Tooltip>
|
||||
) : null
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Search and Filters */}
|
||||
<SearchControls
|
||||
searchTerm={searchParams.keyword}
|
||||
onSearchChange={handleKeywordChange}
|
||||
searchPlaceholder="搜索任务名称、描述"
|
||||
filters={filterOptions}
|
||||
onFiltersChange={handleFiltersChange}
|
||||
viewMode={viewMode}
|
||||
onViewModeChange={setViewMode}
|
||||
showViewToggle={true}
|
||||
onReload={fetchData}
|
||||
onClearFilters={() => setSearchParams({ ...searchParams, filter: {} })}
|
||||
/>
|
||||
{/* Task List */}
|
||||
{viewMode === "card" ? (
|
||||
<CardView
|
||||
data={tableData}
|
||||
operations={taskOperations}
|
||||
pagination={pagination}
|
||||
onView={(tableData) => {
|
||||
navigate("/data/cleansing/task-detail/" + tableData.id)
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Card>
|
||||
<Table
|
||||
columns={taskColumns}
|
||||
dataSource={tableData}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
scroll={{ x: "max-content", y: "calc(100vh - 35rem)" }}
|
||||
pagination={pagination}
|
||||
/>
|
||||
</Card>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
import { useState } from "react";
|
||||
import { Table, Progress, Badge, Button, Tooltip, Card, App } from "antd";
|
||||
import {
|
||||
PlayCircleOutlined,
|
||||
PauseCircleOutlined,
|
||||
DeleteOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { SearchControls } from "@/components/SearchControls";
|
||||
import CardView from "@/components/CardView";
|
||||
import { useNavigate } from "react-router";
|
||||
import { mapTask, TaskStatusMap } from "../../cleansing.const";
|
||||
import {
|
||||
TaskStatus,
|
||||
type CleansingTask,
|
||||
} from "@/pages/DataCleansing/cleansing.model";
|
||||
import useFetchData from "@/hooks/useFetchData";
|
||||
import {
|
||||
deleteCleaningTaskByIdUsingDelete,
|
||||
executeCleaningTaskUsingPost,
|
||||
queryCleaningTasksUsingGet,
|
||||
stopCleaningTaskUsingPost,
|
||||
} from "../../cleansing.api";
|
||||
|
||||
export default function TaskList() {
|
||||
const navigate = useNavigate();
|
||||
const { message } = App.useApp();
|
||||
const [viewMode, setViewMode] = useState<"card" | "list">("list");
|
||||
const filterOptions = [
|
||||
{
|
||||
key: "status",
|
||||
label: "状态",
|
||||
options: [...Object.values(TaskStatusMap)],
|
||||
},
|
||||
];
|
||||
|
||||
const {
|
||||
loading,
|
||||
tableData,
|
||||
pagination,
|
||||
searchParams,
|
||||
setSearchParams,
|
||||
fetchData,
|
||||
handleFiltersChange,
|
||||
handleKeywordChange,
|
||||
} = useFetchData(queryCleaningTasksUsingGet, mapTask);
|
||||
|
||||
const pauseTask = async (item: CleansingTask) => {
|
||||
await stopCleaningTaskUsingPost(item.id);
|
||||
message.success("任务已暂停");
|
||||
fetchData();
|
||||
};
|
||||
|
||||
const startTask = async (item: CleansingTask) => {
|
||||
await executeCleaningTaskUsingPost(item.id);
|
||||
message.success("任务已启动");
|
||||
fetchData();
|
||||
};
|
||||
|
||||
const deleteTask = async (item: CleansingTask) => {
|
||||
await deleteCleaningTaskByIdUsingDelete(item.id);
|
||||
message.success("任务已删除");
|
||||
fetchData();
|
||||
};
|
||||
|
||||
const taskOperations = (record: CleansingTask) => {
|
||||
const isRunning = record.status?.value === TaskStatus.RUNNING;
|
||||
const showStart = [
|
||||
TaskStatus.PENDING,
|
||||
TaskStatus.FAILED,
|
||||
TaskStatus.STOPPED,
|
||||
].includes(record.status?.value);
|
||||
const pauseBtn = {
|
||||
key: "pause",
|
||||
label: "暂停",
|
||||
icon: isRunning ? <PauseCircleOutlined /> : <PlayCircleOutlined />,
|
||||
onClick: pauseTask, // implement pause/play logic
|
||||
};
|
||||
|
||||
const startBtn = {
|
||||
key: "start",
|
||||
label: "启动",
|
||||
icon: isRunning ? <PauseCircleOutlined /> : <PlayCircleOutlined />,
|
||||
onClick: startTask, // implement pause/play logic
|
||||
};
|
||||
return [
|
||||
...(isRunning
|
||||
? [ pauseBtn ]
|
||||
: []),
|
||||
...(showStart
|
||||
? [ startBtn ]
|
||||
: []),
|
||||
{
|
||||
key: "delete",
|
||||
label: "删除",
|
||||
danger: true,
|
||||
icon: <DeleteOutlined />,
|
||||
onClick: deleteTask, // implement delete logic
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
const taskColumns = [
|
||||
{
|
||||
title: "任务名称",
|
||||
dataIndex: "name",
|
||||
key: "name",
|
||||
fixed: "left",
|
||||
width: 150,
|
||||
ellipsis: true,
|
||||
render: (_, task: CleansingTask) => {
|
||||
return (
|
||||
<Button
|
||||
type="link"
|
||||
onClick={() =>
|
||||
navigate("/data/cleansing/task-detail/" + task.id)
|
||||
}
|
||||
>
|
||||
{task.name}
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "任务ID",
|
||||
dataIndex: "id",
|
||||
key: "id",
|
||||
width: 150,
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: "源数据集",
|
||||
dataIndex: "srcDatasetId",
|
||||
key: "srcDatasetId",
|
||||
width: 150,
|
||||
ellipsis: true,
|
||||
render: (_, record: CleansingTask) => {
|
||||
return (
|
||||
<Button
|
||||
type="link"
|
||||
onClick={() =>
|
||||
navigate("/data/management/detail/" + record.srcDatasetId)
|
||||
}
|
||||
>
|
||||
{record.srcDatasetName}
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "目标数据集",
|
||||
dataIndex: "destDatasetId",
|
||||
key: "destDatasetId",
|
||||
width: 150,
|
||||
ellipsis: true,
|
||||
render: (_, record: CleansingTask) => {
|
||||
return (
|
||||
<Button
|
||||
type="link"
|
||||
onClick={() =>
|
||||
navigate("/data/management/detail/" + record.destDatasetId)
|
||||
}
|
||||
>
|
||||
{record.destDatasetName}
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "状态",
|
||||
dataIndex: "status",
|
||||
key: "status",
|
||||
width: 100,
|
||||
render: (status: any) => {
|
||||
return <Badge color={status?.color} text={status?.label} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "进度",
|
||||
dataIndex: "process",
|
||||
key: "process",
|
||||
width: 150,
|
||||
render: (_, record: CleansingTask) => {
|
||||
if (record?.status?.value == TaskStatus.FAILED) {
|
||||
return <Progress percent={record?.progress?.process} size="small" status="exception" />;
|
||||
}
|
||||
return <Progress percent={record?.progress?.process} size="small"/>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "已处理文件数",
|
||||
dataIndex: "finishedFileNum",
|
||||
key: "finishedFileNum",
|
||||
width: 120,
|
||||
align: "right",
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: "总文件数",
|
||||
dataIndex: "totalFileNum",
|
||||
key: "totalFileNum",
|
||||
width: 100,
|
||||
align: "right",
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: "执行耗时",
|
||||
dataIndex: "duration",
|
||||
key: "duration",
|
||||
width: 100,
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: "开始时间",
|
||||
dataIndex: "startedAt",
|
||||
key: "startedAt",
|
||||
width: 180,
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: "结束时间",
|
||||
dataIndex: "finishedAt",
|
||||
key: "finishedAt",
|
||||
width: 180,
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: "创建时间",
|
||||
dataIndex: "createdAt",
|
||||
key: "createdAt",
|
||||
width: 180,
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: "数据量变化",
|
||||
dataIndex: "dataSizeChange",
|
||||
key: "dataSizeChange",
|
||||
width: 180,
|
||||
ellipsis: true,
|
||||
render: (_: any, record: CleansingTask) => {
|
||||
if (record.before !== undefined && record.after !== undefined) {
|
||||
return `${record.before} → ${record.after}`;
|
||||
}
|
||||
return "-";
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
key: "action",
|
||||
fixed: "right",
|
||||
render: (text: string, record: any) => (
|
||||
<div className="flex gap-2">
|
||||
{taskOperations(record).map((op) =>
|
||||
op ? (
|
||||
<Tooltip key={op.key} title={op.label}>
|
||||
<Button
|
||||
type="text"
|
||||
icon={op.icon}
|
||||
danger={op?.danger}
|
||||
onClick={() => op.onClick(record)}
|
||||
/>
|
||||
</Tooltip>
|
||||
) : null
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Search and Filters */}
|
||||
<SearchControls
|
||||
searchTerm={searchParams.keyword}
|
||||
onSearchChange={handleKeywordChange}
|
||||
searchPlaceholder="搜索任务名称、描述"
|
||||
filters={filterOptions}
|
||||
onFiltersChange={handleFiltersChange}
|
||||
viewMode={viewMode}
|
||||
onViewModeChange={setViewMode}
|
||||
showViewToggle={true}
|
||||
onReload={fetchData}
|
||||
onClearFilters={() => setSearchParams({ ...searchParams, filter: {} })}
|
||||
/>
|
||||
{/* Task List */}
|
||||
{viewMode === "card" ? (
|
||||
<CardView
|
||||
data={tableData}
|
||||
operations={taskOperations}
|
||||
pagination={pagination}
|
||||
onView={(tableData) => {
|
||||
navigate("/data/cleansing/task-detail/" + tableData.id)
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Card>
|
||||
<Table
|
||||
columns={taskColumns}
|
||||
dataSource={tableData}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
scroll={{ x: "max-content", y: "calc(100vh - 35rem)" }}
|
||||
pagination={pagination}
|
||||
/>
|
||||
</Card>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,156 +1,156 @@
|
||||
import {DeleteOutlined, EditOutlined} from "@ant-design/icons";
|
||||
import CardView from "@/components/CardView";
|
||||
import {
|
||||
deleteCleaningTemplateByIdUsingDelete, queryCleaningTemplatesUsingGet,
|
||||
} from "../../cleansing.api";
|
||||
import useFetchData from "@/hooks/useFetchData";
|
||||
import {mapTemplate} from "../../cleansing.const";
|
||||
import {App, Button, Card, Table, Tooltip} from "antd";
|
||||
import {CleansingTemplate} from "../../cleansing.model";
|
||||
import {SearchControls} from "@/components/SearchControls.tsx";
|
||||
import {useNavigate} from "react-router";
|
||||
import {useState} from "react";
|
||||
|
||||
export default function TemplateList() {
|
||||
const navigate = useNavigate();
|
||||
const { message } = App.useApp();
|
||||
const [viewMode, setViewMode] = useState<"card" | "list">("list");
|
||||
|
||||
const {
|
||||
loading,
|
||||
tableData,
|
||||
pagination,
|
||||
searchParams,
|
||||
setSearchParams,
|
||||
fetchData,
|
||||
handleFiltersChange,
|
||||
handleKeywordChange,
|
||||
} = useFetchData(queryCleaningTemplatesUsingGet, mapTemplate);
|
||||
|
||||
const templateOperations = () => {
|
||||
return [
|
||||
{
|
||||
key: "update",
|
||||
label: "编辑",
|
||||
icon: <EditOutlined />,
|
||||
onClick: (template: CleansingTemplate) => navigate(`/data/cleansing/update-template/${template.id}`)
|
||||
},
|
||||
{
|
||||
key: "delete",
|
||||
label: "删除",
|
||||
danger: true,
|
||||
icon: <DeleteOutlined />,
|
||||
onClick: deleteTemplate, // implement delete logic
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
const templateColumns = [
|
||||
{
|
||||
title: "模板名称",
|
||||
dataIndex: "name",
|
||||
key: "name",
|
||||
fixed: "left",
|
||||
width: 150,
|
||||
ellipsis: true,
|
||||
render: (_, template: CleansingTemplate) => {
|
||||
return (
|
||||
<Button
|
||||
type="link"
|
||||
onClick={() =>
|
||||
navigate("/data/cleansing/template-detail/" + template.id)
|
||||
}
|
||||
>
|
||||
{template.name}
|
||||
</Button>
|
||||
);
|
||||
}},
|
||||
{
|
||||
title: "模板ID",
|
||||
dataIndex: "id",
|
||||
key: "id",
|
||||
fixed: "left",
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
title: "算子数量",
|
||||
dataIndex: "num",
|
||||
key: "num",
|
||||
width: 100,
|
||||
ellipsis: true,
|
||||
render: (_, template: CleansingTemplate) => {
|
||||
return template.instance?.length ?? 0;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
key: "action",
|
||||
fixed: "right",
|
||||
width: 20,
|
||||
render: (text: string, record: any) => (
|
||||
<div className="flex gap-2">
|
||||
{templateOperations(record).map((op) =>
|
||||
op ? (
|
||||
<Tooltip key={op.key} title={op.label}>
|
||||
<Button
|
||||
type="text"
|
||||
icon={op.icon}
|
||||
danger={op?.danger}
|
||||
onClick={() => op.onClick(record)}
|
||||
/>
|
||||
</Tooltip>
|
||||
) : null
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
const deleteTemplate = async (template: CleansingTemplate) => {
|
||||
if (!template.id) {
|
||||
return;
|
||||
}
|
||||
// 实现删除逻辑
|
||||
await deleteCleaningTemplateByIdUsingDelete(template.id);
|
||||
fetchData();
|
||||
message.success("模板删除成功");
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Search and Filters */}
|
||||
<SearchControls
|
||||
searchTerm={searchParams.keyword}
|
||||
onSearchChange={handleKeywordChange}
|
||||
searchPlaceholder="搜索模板名称、描述"
|
||||
onFiltersChange={handleFiltersChange}
|
||||
viewMode={viewMode}
|
||||
onViewModeChange={setViewMode}
|
||||
showViewToggle={true}
|
||||
onReload={fetchData}
|
||||
onClearFilters={() => setSearchParams({ ...searchParams, filter: {} })}
|
||||
/>
|
||||
{viewMode === "card" ? (
|
||||
<CardView
|
||||
data={tableData}
|
||||
operations={templateOperations}
|
||||
pagination={pagination}
|
||||
onView={(tableData) => {
|
||||
navigate("/data/cleansing/template-detail/" + tableData.id)
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Card>
|
||||
<Table
|
||||
columns={templateColumns}
|
||||
dataSource={tableData}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
scroll={{ x: "max-content", y: "calc(100vh - 35rem)" }}
|
||||
pagination={pagination}
|
||||
/>
|
||||
</Card>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
import {DeleteOutlined, EditOutlined} from "@ant-design/icons";
|
||||
import CardView from "@/components/CardView";
|
||||
import {
|
||||
deleteCleaningTemplateByIdUsingDelete, queryCleaningTemplatesUsingGet,
|
||||
} from "../../cleansing.api";
|
||||
import useFetchData from "@/hooks/useFetchData";
|
||||
import {mapTemplate} from "../../cleansing.const";
|
||||
import {App, Button, Card, Table, Tooltip} from "antd";
|
||||
import {CleansingTemplate} from "../../cleansing.model";
|
||||
import {SearchControls} from "@/components/SearchControls.tsx";
|
||||
import {useNavigate} from "react-router";
|
||||
import {useState} from "react";
|
||||
|
||||
export default function TemplateList() {
|
||||
const navigate = useNavigate();
|
||||
const { message } = App.useApp();
|
||||
const [viewMode, setViewMode] = useState<"card" | "list">("list");
|
||||
|
||||
const {
|
||||
loading,
|
||||
tableData,
|
||||
pagination,
|
||||
searchParams,
|
||||
setSearchParams,
|
||||
fetchData,
|
||||
handleFiltersChange,
|
||||
handleKeywordChange,
|
||||
} = useFetchData(queryCleaningTemplatesUsingGet, mapTemplate);
|
||||
|
||||
const templateOperations = () => {
|
||||
return [
|
||||
{
|
||||
key: "update",
|
||||
label: "编辑",
|
||||
icon: <EditOutlined />,
|
||||
onClick: (template: CleansingTemplate) => navigate(`/data/cleansing/update-template/${template.id}`)
|
||||
},
|
||||
{
|
||||
key: "delete",
|
||||
label: "删除",
|
||||
danger: true,
|
||||
icon: <DeleteOutlined />,
|
||||
onClick: deleteTemplate, // implement delete logic
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
const templateColumns = [
|
||||
{
|
||||
title: "模板名称",
|
||||
dataIndex: "name",
|
||||
key: "name",
|
||||
fixed: "left",
|
||||
width: 150,
|
||||
ellipsis: true,
|
||||
render: (_, template: CleansingTemplate) => {
|
||||
return (
|
||||
<Button
|
||||
type="link"
|
||||
onClick={() =>
|
||||
navigate("/data/cleansing/template-detail/" + template.id)
|
||||
}
|
||||
>
|
||||
{template.name}
|
||||
</Button>
|
||||
);
|
||||
}},
|
||||
{
|
||||
title: "模板ID",
|
||||
dataIndex: "id",
|
||||
key: "id",
|
||||
fixed: "left",
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
title: "算子数量",
|
||||
dataIndex: "num",
|
||||
key: "num",
|
||||
width: 100,
|
||||
ellipsis: true,
|
||||
render: (_, template: CleansingTemplate) => {
|
||||
return template.instance?.length ?? 0;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
key: "action",
|
||||
fixed: "right",
|
||||
width: 20,
|
||||
render: (text: string, record: any) => (
|
||||
<div className="flex gap-2">
|
||||
{templateOperations(record).map((op) =>
|
||||
op ? (
|
||||
<Tooltip key={op.key} title={op.label}>
|
||||
<Button
|
||||
type="text"
|
||||
icon={op.icon}
|
||||
danger={op?.danger}
|
||||
onClick={() => op.onClick(record)}
|
||||
/>
|
||||
</Tooltip>
|
||||
) : null
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
const deleteTemplate = async (template: CleansingTemplate) => {
|
||||
if (!template.id) {
|
||||
return;
|
||||
}
|
||||
// 实现删除逻辑
|
||||
await deleteCleaningTemplateByIdUsingDelete(template.id);
|
||||
fetchData();
|
||||
message.success("模板删除成功");
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Search and Filters */}
|
||||
<SearchControls
|
||||
searchTerm={searchParams.keyword}
|
||||
onSearchChange={handleKeywordChange}
|
||||
searchPlaceholder="搜索模板名称、描述"
|
||||
onFiltersChange={handleFiltersChange}
|
||||
viewMode={viewMode}
|
||||
onViewModeChange={setViewMode}
|
||||
showViewToggle={true}
|
||||
onReload={fetchData}
|
||||
onClearFilters={() => setSearchParams({ ...searchParams, filter: {} })}
|
||||
/>
|
||||
{viewMode === "card" ? (
|
||||
<CardView
|
||||
data={tableData}
|
||||
operations={templateOperations}
|
||||
pagination={pagination}
|
||||
onView={(tableData) => {
|
||||
navigate("/data/cleansing/template-detail/" + tableData.id)
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Card>
|
||||
<Table
|
||||
columns={templateColumns}
|
||||
dataSource={tableData}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
scroll={{ x: "max-content", y: "calc(100vh - 35rem)" }}
|
||||
pagination={pagination}
|
||||
/>
|
||||
</Card>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,65 +1,65 @@
|
||||
import { get, post, put, del } from "@/utils/request";
|
||||
|
||||
// 清洗任务相关接口
|
||||
export function queryCleaningTasksUsingGet(params?: any) {
|
||||
return get("/api/cleaning/tasks", params);
|
||||
}
|
||||
|
||||
export function createCleaningTaskUsingPost(data: any) {
|
||||
return post("/api/cleaning/tasks", data);
|
||||
}
|
||||
|
||||
export function queryCleaningTaskByIdUsingGet(taskId: string | number) {
|
||||
return get(`/api/cleaning/tasks/${taskId}`);
|
||||
}
|
||||
|
||||
export function queryCleaningTaskResultByIdUsingGet(taskId: string | number) {
|
||||
return get(`/api/cleaning/tasks/${taskId}/result`);
|
||||
}
|
||||
|
||||
export function queryCleaningTaskLogByIdUsingGet(taskId: string | number) {
|
||||
return get(`/api/cleaning/tasks/${taskId}/log`);
|
||||
}
|
||||
|
||||
export function updateCleaningTaskByIdUsingPut(taskId: string | number, data: any) {
|
||||
return put(`/api/cleaning/tasks/${taskId}`, data);
|
||||
}
|
||||
|
||||
export function deleteCleaningTaskByIdUsingDelete(taskId: string | number) {
|
||||
return del(`/api/cleaning/tasks/${taskId}`);
|
||||
}
|
||||
|
||||
export function executeCleaningTaskUsingPost(taskId: string | number, data?: any) {
|
||||
return post(`/api/cleaning/tasks/${taskId}/execute`, data);
|
||||
}
|
||||
|
||||
export function stopCleaningTaskUsingPost(taskId: string | number, data?: any) {
|
||||
return post(`/api/cleaning/tasks/${taskId}/stop`, data);
|
||||
}
|
||||
|
||||
// 清洗模板相关接口
|
||||
export function queryCleaningTemplatesUsingGet(params?: any) {
|
||||
return get("/api/cleaning/templates", params);
|
||||
}
|
||||
|
||||
export function createCleaningTemplateUsingPost(data: any) {
|
||||
return post("/api/cleaning/templates", data);
|
||||
}
|
||||
|
||||
export function queryCleaningTemplateByIdUsingGet(templateId: string | number) {
|
||||
return get(`/api/cleaning/templates/${templateId}`);
|
||||
}
|
||||
|
||||
export function updateCleaningTemplateByIdUsingPut(templateId: string | number, data: any) {
|
||||
return put(`/api/cleaning/templates/${templateId}`, data);
|
||||
}
|
||||
|
||||
export function deleteCleaningTemplateByIdUsingDelete(templateId: string | number) {
|
||||
return del(`/api/cleaning/templates/${templateId}`);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
import { get, post, put, del } from "@/utils/request";
|
||||
|
||||
// 清洗任务相关接口
|
||||
export function queryCleaningTasksUsingGet(params?: any) {
|
||||
return get("/api/cleaning/tasks", params);
|
||||
}
|
||||
|
||||
export function createCleaningTaskUsingPost(data: any) {
|
||||
return post("/api/cleaning/tasks", data);
|
||||
}
|
||||
|
||||
export function queryCleaningTaskByIdUsingGet(taskId: string | number) {
|
||||
return get(`/api/cleaning/tasks/${taskId}`);
|
||||
}
|
||||
|
||||
export function queryCleaningTaskResultByIdUsingGet(taskId: string | number) {
|
||||
return get(`/api/cleaning/tasks/${taskId}/result`);
|
||||
}
|
||||
|
||||
export function queryCleaningTaskLogByIdUsingGet(taskId: string | number) {
|
||||
return get(`/api/cleaning/tasks/${taskId}/log`);
|
||||
}
|
||||
|
||||
export function updateCleaningTaskByIdUsingPut(taskId: string | number, data: any) {
|
||||
return put(`/api/cleaning/tasks/${taskId}`, data);
|
||||
}
|
||||
|
||||
export function deleteCleaningTaskByIdUsingDelete(taskId: string | number) {
|
||||
return del(`/api/cleaning/tasks/${taskId}`);
|
||||
}
|
||||
|
||||
export function executeCleaningTaskUsingPost(taskId: string | number, data?: any) {
|
||||
return post(`/api/cleaning/tasks/${taskId}/execute`, data);
|
||||
}
|
||||
|
||||
export function stopCleaningTaskUsingPost(taskId: string | number, data?: any) {
|
||||
return post(`/api/cleaning/tasks/${taskId}/stop`, data);
|
||||
}
|
||||
|
||||
// 清洗模板相关接口
|
||||
export function queryCleaningTemplatesUsingGet(params?: any) {
|
||||
return get("/api/cleaning/templates", params);
|
||||
}
|
||||
|
||||
export function createCleaningTemplateUsingPost(data: any) {
|
||||
return post("/api/cleaning/templates", data);
|
||||
}
|
||||
|
||||
export function queryCleaningTemplateByIdUsingGet(templateId: string | number) {
|
||||
return get(`/api/cleaning/templates/${templateId}`);
|
||||
}
|
||||
|
||||
export function updateCleaningTemplateByIdUsingPut(templateId: string | number, data: any) {
|
||||
return put(`/api/cleaning/templates/${templateId}`, data);
|
||||
}
|
||||
|
||||
export function deleteCleaningTemplateByIdUsingDelete(templateId: string | number) {
|
||||
return del(`/api/cleaning/templates/${templateId}`);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,137 +1,137 @@
|
||||
import {
|
||||
CleansingTask,
|
||||
CleansingTemplate,
|
||||
TaskStatus,
|
||||
TemplateType,
|
||||
} from "@/pages/DataCleansing/cleansing.model";
|
||||
import {
|
||||
formatBytes,
|
||||
formatDateTime,
|
||||
formatExecutionDuration,
|
||||
} from "@/utils/unit";
|
||||
import {
|
||||
ClockCircleOutlined,
|
||||
PlayCircleOutlined,
|
||||
CheckCircleOutlined,
|
||||
AlertOutlined,
|
||||
PauseCircleOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { BrushCleaning, Layout } from "lucide-react";
|
||||
|
||||
export const templateTypesMap = {
|
||||
[TemplateType.TEXT]: {
|
||||
label: "文本",
|
||||
value: TemplateType.TEXT,
|
||||
icon: "📝",
|
||||
description: "处理文本数据的清洗模板",
|
||||
},
|
||||
[TemplateType.IMAGE]: {
|
||||
label: "图片",
|
||||
value: TemplateType.IMAGE,
|
||||
icon: "🖼️",
|
||||
description: "处理图像数据的清洗模板",
|
||||
},
|
||||
[TemplateType.VIDEO]: {
|
||||
value: TemplateType.VIDEO,
|
||||
label: "视频",
|
||||
icon: "🎥",
|
||||
description: "处理视频数据的清洗模板",
|
||||
},
|
||||
[TemplateType.AUDIO]: {
|
||||
value: TemplateType.AUDIO,
|
||||
label: "音频",
|
||||
icon: "🎵",
|
||||
description: "处理音频数据的清洗模板",
|
||||
},
|
||||
[TemplateType.IMAGE2TEXT]: {
|
||||
value: TemplateType.IMAGE2TEXT,
|
||||
label: "图片转文本",
|
||||
icon: "🔄",
|
||||
description: "图像识别转文本的处理模板",
|
||||
},
|
||||
};
|
||||
|
||||
export const TaskStatusMap = {
|
||||
[TaskStatus.PENDING]: {
|
||||
label: "待处理",
|
||||
value: TaskStatus.PENDING,
|
||||
color: "gray",
|
||||
icon: <ClockCircleOutlined />,
|
||||
},
|
||||
[TaskStatus.RUNNING]: {
|
||||
label: "进行中",
|
||||
value: TaskStatus.RUNNING,
|
||||
color: "blue",
|
||||
icon: <PlayCircleOutlined />,
|
||||
},
|
||||
[TaskStatus.COMPLETED]: {
|
||||
label: "已完成",
|
||||
value: TaskStatus.COMPLETED,
|
||||
color: "green",
|
||||
icon: <CheckCircleOutlined />,
|
||||
},
|
||||
[TaskStatus.FAILED]: {
|
||||
label: "失败",
|
||||
value: TaskStatus.FAILED,
|
||||
color: "red",
|
||||
icon: <AlertOutlined />,
|
||||
},
|
||||
[TaskStatus.STOPPED]: {
|
||||
label: "已停止",
|
||||
value: TaskStatus.STOPPED,
|
||||
color: "orange",
|
||||
icon: <PauseCircleOutlined />,
|
||||
},
|
||||
};
|
||||
|
||||
export const mapTask = (task: CleansingTask) => {
|
||||
const duration = formatExecutionDuration(task.startedAt, task.finishedAt);
|
||||
const before = formatBytes(task.beforeSize);
|
||||
const after = formatBytes(task.afterSize);
|
||||
const status = TaskStatusMap[task.status];
|
||||
const finishedAt = formatDateTime(task.finishedAt);
|
||||
const startedAt = formatDateTime(task.startedAt);
|
||||
const createdAt = formatDateTime(task.createdAt);
|
||||
return {
|
||||
...task,
|
||||
...task.progress,
|
||||
createdAt,
|
||||
startedAt,
|
||||
finishedAt,
|
||||
updatedAt: formatDateTime(
|
||||
new Date(Math.max(...[
|
||||
new Date(task.finishedAt).getTime(),
|
||||
new Date(task.startedAt).getTime(),
|
||||
new Date(task.createdAt).getTime()])).toISOString()),
|
||||
icon: <BrushCleaning className="w-full h-full" />,
|
||||
status,
|
||||
duration,
|
||||
before,
|
||||
after,
|
||||
statistics: [
|
||||
{ label: "进度", value: `${task?.progress?.process || 0}%` },
|
||||
{
|
||||
label: "执行耗时",
|
||||
value: duration,
|
||||
},
|
||||
{
|
||||
label: "已处理文件数",
|
||||
value: task?.progress?.finishedFileNum || 0,
|
||||
},
|
||||
{
|
||||
label: "总文件数",
|
||||
value: task?.progress?.totalFileNum || 0,
|
||||
},
|
||||
],
|
||||
lastModified: formatDateTime(task.createdAt),
|
||||
};
|
||||
};
|
||||
|
||||
export const mapTemplate = (template: CleansingTemplate) => ({
|
||||
...template,
|
||||
createdAt: formatDateTime(template.createdAt),
|
||||
updatedAt: formatDateTime(template.updatedAt),
|
||||
icon: <Layout className="w-full h-full" />,
|
||||
statistics: [{ label: "算子数量", value: template.instance?.length ?? 0 }],
|
||||
lastModified: formatDateTime(template.updatedAt),
|
||||
});
|
||||
import {
|
||||
CleansingTask,
|
||||
CleansingTemplate,
|
||||
TaskStatus,
|
||||
TemplateType,
|
||||
} from "@/pages/DataCleansing/cleansing.model";
|
||||
import {
|
||||
formatBytes,
|
||||
formatDateTime,
|
||||
formatExecutionDuration,
|
||||
} from "@/utils/unit";
|
||||
import {
|
||||
ClockCircleOutlined,
|
||||
PlayCircleOutlined,
|
||||
CheckCircleOutlined,
|
||||
AlertOutlined,
|
||||
PauseCircleOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { BrushCleaning, Layout } from "lucide-react";
|
||||
|
||||
export const templateTypesMap = {
|
||||
[TemplateType.TEXT]: {
|
||||
label: "文本",
|
||||
value: TemplateType.TEXT,
|
||||
icon: "📝",
|
||||
description: "处理文本数据的清洗模板",
|
||||
},
|
||||
[TemplateType.IMAGE]: {
|
||||
label: "图片",
|
||||
value: TemplateType.IMAGE,
|
||||
icon: "🖼️",
|
||||
description: "处理图像数据的清洗模板",
|
||||
},
|
||||
[TemplateType.VIDEO]: {
|
||||
value: TemplateType.VIDEO,
|
||||
label: "视频",
|
||||
icon: "🎥",
|
||||
description: "处理视频数据的清洗模板",
|
||||
},
|
||||
[TemplateType.AUDIO]: {
|
||||
value: TemplateType.AUDIO,
|
||||
label: "音频",
|
||||
icon: "🎵",
|
||||
description: "处理音频数据的清洗模板",
|
||||
},
|
||||
[TemplateType.IMAGE2TEXT]: {
|
||||
value: TemplateType.IMAGE2TEXT,
|
||||
label: "图片转文本",
|
||||
icon: "🔄",
|
||||
description: "图像识别转文本的处理模板",
|
||||
},
|
||||
};
|
||||
|
||||
export const TaskStatusMap = {
|
||||
[TaskStatus.PENDING]: {
|
||||
label: "待处理",
|
||||
value: TaskStatus.PENDING,
|
||||
color: "gray",
|
||||
icon: <ClockCircleOutlined />,
|
||||
},
|
||||
[TaskStatus.RUNNING]: {
|
||||
label: "进行中",
|
||||
value: TaskStatus.RUNNING,
|
||||
color: "blue",
|
||||
icon: <PlayCircleOutlined />,
|
||||
},
|
||||
[TaskStatus.COMPLETED]: {
|
||||
label: "已完成",
|
||||
value: TaskStatus.COMPLETED,
|
||||
color: "green",
|
||||
icon: <CheckCircleOutlined />,
|
||||
},
|
||||
[TaskStatus.FAILED]: {
|
||||
label: "失败",
|
||||
value: TaskStatus.FAILED,
|
||||
color: "red",
|
||||
icon: <AlertOutlined />,
|
||||
},
|
||||
[TaskStatus.STOPPED]: {
|
||||
label: "已停止",
|
||||
value: TaskStatus.STOPPED,
|
||||
color: "orange",
|
||||
icon: <PauseCircleOutlined />,
|
||||
},
|
||||
};
|
||||
|
||||
export const mapTask = (task: CleansingTask) => {
|
||||
const duration = formatExecutionDuration(task.startedAt, task.finishedAt);
|
||||
const before = formatBytes(task.beforeSize);
|
||||
const after = formatBytes(task.afterSize);
|
||||
const status = TaskStatusMap[task.status];
|
||||
const finishedAt = formatDateTime(task.finishedAt);
|
||||
const startedAt = formatDateTime(task.startedAt);
|
||||
const createdAt = formatDateTime(task.createdAt);
|
||||
return {
|
||||
...task,
|
||||
...task.progress,
|
||||
createdAt,
|
||||
startedAt,
|
||||
finishedAt,
|
||||
updatedAt: formatDateTime(
|
||||
new Date(Math.max(...[
|
||||
new Date(task.finishedAt).getTime(),
|
||||
new Date(task.startedAt).getTime(),
|
||||
new Date(task.createdAt).getTime()])).toISOString()),
|
||||
icon: <BrushCleaning className="w-full h-full" />,
|
||||
status,
|
||||
duration,
|
||||
before,
|
||||
after,
|
||||
statistics: [
|
||||
{ label: "进度", value: `${task?.progress?.process || 0}%` },
|
||||
{
|
||||
label: "执行耗时",
|
||||
value: duration,
|
||||
},
|
||||
{
|
||||
label: "已处理文件数",
|
||||
value: task?.progress?.finishedFileNum || 0,
|
||||
},
|
||||
{
|
||||
label: "总文件数",
|
||||
value: task?.progress?.totalFileNum || 0,
|
||||
},
|
||||
],
|
||||
lastModified: formatDateTime(task.createdAt),
|
||||
};
|
||||
};
|
||||
|
||||
export const mapTemplate = (template: CleansingTemplate) => ({
|
||||
...template,
|
||||
createdAt: formatDateTime(template.createdAt),
|
||||
updatedAt: formatDateTime(template.updatedAt),
|
||||
icon: <Layout className="w-full h-full" />,
|
||||
statistics: [{ label: "算子数量", value: template.instance?.length ?? 0 }],
|
||||
lastModified: formatDateTime(template.updatedAt),
|
||||
});
|
||||
|
||||
@@ -1,89 +1,89 @@
|
||||
import { OperatorI } from "../OperatorMarket/operator.model";
|
||||
|
||||
export interface CleansingTask {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
srcDatasetId: string;
|
||||
srcDatasetName: string;
|
||||
destDatasetId: string;
|
||||
destDatasetName: string;
|
||||
templateId: string;
|
||||
templateName: string;
|
||||
status: {
|
||||
label: string;
|
||||
value: TaskStatus;
|
||||
color: string;
|
||||
};
|
||||
startedAt: string;
|
||||
progress: {
|
||||
finishedFileNum: number;
|
||||
succeedFileNum: number;
|
||||
failedFileNum: number;
|
||||
process: 100;
|
||||
totalFileNum: number;
|
||||
successRate: 100;
|
||||
};
|
||||
instance: OperatorI[];
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
finishedAt: string;
|
||||
beforeSize?: number;
|
||||
afterSize?: number;
|
||||
}
|
||||
|
||||
export interface CleansingTemplate {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
instance: OperatorI[];
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export enum RuleCategory {
|
||||
DATA_VALIDATION = "DATA_VALIDATION",
|
||||
MISSING_VALUE_HANDLING = "MISSING_VALUE_HANDLING",
|
||||
OUTLIER_DETECTION = "OUTLIER_DETECTION",
|
||||
DEDUPLICATION = "DEDUPLICATION",
|
||||
FORMAT_STANDARDIZATION = "FORMAT_STANDARDIZATION",
|
||||
TEXT_CLEANING = "TEXT_CLEANING",
|
||||
CUSTOM = "CUSTOM",
|
||||
}
|
||||
|
||||
export enum TaskStatus {
|
||||
PENDING = "PENDING",
|
||||
RUNNING = "RUNNING",
|
||||
COMPLETED = "COMPLETED",
|
||||
FAILED = "FAILED",
|
||||
STOPPED = "STOPPED",
|
||||
}
|
||||
|
||||
export interface RuleCondition {
|
||||
field: string;
|
||||
operator: string;
|
||||
value: string;
|
||||
logicOperator?: "AND" | "OR";
|
||||
}
|
||||
|
||||
export enum TemplateType {
|
||||
TEXT = "TEXT",
|
||||
IMAGE = "IMAGE",
|
||||
VIDEO = "VIDEO",
|
||||
AUDIO = "AUDIO",
|
||||
IMAGE2TEXT = "IMAGE2TEXT",
|
||||
}
|
||||
|
||||
export interface CleansingResult {
|
||||
instanceId: string;
|
||||
srcFileId: string;
|
||||
destFileId: string;
|
||||
srcName: string;
|
||||
destName: string;
|
||||
srcType: string;
|
||||
destType: string;
|
||||
srcSize: number;
|
||||
destSize: number;
|
||||
status: string;
|
||||
result: string;
|
||||
import { OperatorI } from "../OperatorMarket/operator.model";
|
||||
|
||||
export interface CleansingTask {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
srcDatasetId: string;
|
||||
srcDatasetName: string;
|
||||
destDatasetId: string;
|
||||
destDatasetName: string;
|
||||
templateId: string;
|
||||
templateName: string;
|
||||
status: {
|
||||
label: string;
|
||||
value: TaskStatus;
|
||||
color: string;
|
||||
};
|
||||
startedAt: string;
|
||||
progress: {
|
||||
finishedFileNum: number;
|
||||
succeedFileNum: number;
|
||||
failedFileNum: number;
|
||||
process: 100;
|
||||
totalFileNum: number;
|
||||
successRate: 100;
|
||||
};
|
||||
instance: OperatorI[];
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
finishedAt: string;
|
||||
beforeSize?: number;
|
||||
afterSize?: number;
|
||||
}
|
||||
|
||||
export interface CleansingTemplate {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
instance: OperatorI[];
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export enum RuleCategory {
|
||||
DATA_VALIDATION = "DATA_VALIDATION",
|
||||
MISSING_VALUE_HANDLING = "MISSING_VALUE_HANDLING",
|
||||
OUTLIER_DETECTION = "OUTLIER_DETECTION",
|
||||
DEDUPLICATION = "DEDUPLICATION",
|
||||
FORMAT_STANDARDIZATION = "FORMAT_STANDARDIZATION",
|
||||
TEXT_CLEANING = "TEXT_CLEANING",
|
||||
CUSTOM = "CUSTOM",
|
||||
}
|
||||
|
||||
export enum TaskStatus {
|
||||
PENDING = "PENDING",
|
||||
RUNNING = "RUNNING",
|
||||
COMPLETED = "COMPLETED",
|
||||
FAILED = "FAILED",
|
||||
STOPPED = "STOPPED",
|
||||
}
|
||||
|
||||
export interface RuleCondition {
|
||||
field: string;
|
||||
operator: string;
|
||||
value: string;
|
||||
logicOperator?: "AND" | "OR";
|
||||
}
|
||||
|
||||
export enum TemplateType {
|
||||
TEXT = "TEXT",
|
||||
IMAGE = "IMAGE",
|
||||
VIDEO = "VIDEO",
|
||||
AUDIO = "AUDIO",
|
||||
IMAGE2TEXT = "IMAGE2TEXT",
|
||||
}
|
||||
|
||||
export interface CleansingResult {
|
||||
instanceId: string;
|
||||
srcFileId: string;
|
||||
destFileId: string;
|
||||
srcName: string;
|
||||
destName: string;
|
||||
srcType: string;
|
||||
destType: string;
|
||||
srcSize: number;
|
||||
destSize: number;
|
||||
status: string;
|
||||
result: string;
|
||||
}
|
||||
@@ -1,470 +1,470 @@
|
||||
import { useState } from "react";
|
||||
import { Input, Button, Radio, Form, App, Select } from "antd";
|
||||
import { Link, useNavigate } from "react-router";
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
import { createTaskUsingPost } from "../collection.apis";
|
||||
import SimpleCronScheduler from "@/pages/DataCollection/Create/SimpleCronScheduler";
|
||||
import RadioCard from "@/components/RadioCard";
|
||||
import { datasetTypes } from "@/pages/DataManagement/dataset.const";
|
||||
import { SyncModeMap } from "../collection.const";
|
||||
import { SyncMode } from "../collection.model";
|
||||
import { DatasetSubType } from "@/pages/DataManagement/dataset.model";
|
||||
|
||||
const { TextArea } = Input;
|
||||
|
||||
const defaultTemplates = [
|
||||
{
|
||||
id: "NAS",
|
||||
name: "NAS到本地",
|
||||
description: "从NAS文件系统导入数据到本地文件系统",
|
||||
config: {
|
||||
reader: "nfsreader",
|
||||
writer: "localwriter",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "OBS",
|
||||
name: "OBS到本地",
|
||||
description: "从OBS文件系统导入数据到本地文件系统",
|
||||
config: {
|
||||
reader: "obsreader",
|
||||
writer: "localwriter",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "MYSQL",
|
||||
name: "Mysql到本地",
|
||||
description: "从Mysql中导入数据到本地文件系统",
|
||||
config: {
|
||||
reader: "mysqlreader",
|
||||
writer: "localwriter",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const syncModeOptions = Object.values(SyncModeMap);
|
||||
|
||||
enum TemplateType {
|
||||
NAS = "NAS",
|
||||
OBS = "OBS",
|
||||
MYSQL = "MYSQL",
|
||||
}
|
||||
|
||||
export default function CollectionTaskCreate() {
|
||||
const navigate = useNavigate();
|
||||
const [form] = Form.useForm();
|
||||
const { message } = App.useApp();
|
||||
|
||||
const [templateType, setTemplateType] = useState<"default" | "custom">(
|
||||
"default"
|
||||
);
|
||||
// 默认模板类型设为 NAS
|
||||
const [selectedTemplate, setSelectedTemplate] = useState<TemplateType>(
|
||||
TemplateType.NAS
|
||||
);
|
||||
const [customConfig, setCustomConfig] = useState("");
|
||||
|
||||
// 将 newTask 设为 any,并初始化 config.templateType 为 NAS
|
||||
const [newTask, setNewTask] = useState<any>({
|
||||
name: "",
|
||||
description: "",
|
||||
syncMode: SyncMode.ONCE,
|
||||
cronExpression: "",
|
||||
maxRetries: 10,
|
||||
dataset: null,
|
||||
config: { templateType: TemplateType.NAS },
|
||||
createDataset: false,
|
||||
});
|
||||
const [scheduleExpression, setScheduleExpression] = useState({
|
||||
type: "once",
|
||||
time: "00:00",
|
||||
cronExpression: "0 0 0 * * ?",
|
||||
});
|
||||
|
||||
const [isCreateDataset, setIsCreateDataset] = useState(false);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
await form.validateFields();
|
||||
if (templateType === "default" && !selectedTemplate) {
|
||||
window.alert("请选择默认模板");
|
||||
return;
|
||||
}
|
||||
if (templateType === "custom" && !customConfig.trim()) {
|
||||
window.alert("请填写自定义配置");
|
||||
return;
|
||||
}
|
||||
|
||||
// 构建最终 payload,不依赖异步 setState
|
||||
const payload = {
|
||||
...newTask,
|
||||
taskType:
|
||||
templateType === "default" ? selectedTemplate : "CUSTOM",
|
||||
config: {
|
||||
...((newTask && newTask.config) || {}),
|
||||
...(templateType === "custom" ? { dataxJson: customConfig } : {}),
|
||||
},
|
||||
};
|
||||
|
||||
console.log("创建任务 payload:", payload);
|
||||
|
||||
await createTaskUsingPost(payload);
|
||||
message.success("任务创建成功");
|
||||
navigate("/data/collection");
|
||||
} catch (error) {
|
||||
message.error(`${error?.data?.message}:${error?.data?.data}`);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center">
|
||||
<Link to="/data/collection">
|
||||
<Button type="text">
|
||||
<ArrowLeft className="w-4 h-4 mr-1" />
|
||||
</Button>
|
||||
</Link>
|
||||
<h1 className="text-xl font-bold bg-clip-text">创建归集任务</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-overflow-auto border-card">
|
||||
<div className="flex-1 overflow-auto p-6">
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
initialValues={newTask}
|
||||
onValuesChange={(_, allValues) => {
|
||||
setNewTask({ ...newTask, ...allValues });
|
||||
}}
|
||||
>
|
||||
{/* 基本信息 */}
|
||||
<h2 className="font-medium text-gray-900 text-lg mb-2">基本信息</h2>
|
||||
|
||||
<Form.Item
|
||||
label="名称"
|
||||
name="name"
|
||||
rules={[{ required: true, message: "请输入任务名称" }]}
|
||||
>
|
||||
<Input placeholder="请输入任务名称" />
|
||||
</Form.Item>
|
||||
<Form.Item label="描述" name="description">
|
||||
<TextArea placeholder="请输入任务描述" rows={3} />
|
||||
</Form.Item>
|
||||
|
||||
{/* 同步配置 */}
|
||||
<h2 className="font-medium text-gray-900 pt-6 mb-2 text-lg">
|
||||
同步配置
|
||||
</h2>
|
||||
<Form.Item name="syncMode" label="同步方式">
|
||||
<Radio.Group
|
||||
value={newTask.syncMode}
|
||||
options={syncModeOptions}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
setNewTask({
|
||||
...newTask,
|
||||
syncMode: value,
|
||||
scheduleExpression:
|
||||
value === SyncMode.SCHEDULED
|
||||
? scheduleExpression.cronExpression
|
||||
: "",
|
||||
});
|
||||
}}
|
||||
></Radio.Group>
|
||||
</Form.Item>
|
||||
{newTask.syncMode === SyncMode.SCHEDULED && (
|
||||
<Form.Item
|
||||
label=""
|
||||
rules={[{ required: true, message: "请输入Cron表达式" }]}
|
||||
>
|
||||
<SimpleCronScheduler
|
||||
className="px-2 rounded"
|
||||
value={scheduleExpression}
|
||||
onChange={(value) => {
|
||||
setScheduleExpression(value);
|
||||
setNewTask({
|
||||
...newTask,
|
||||
scheduleExpression: value.cronExpression,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
{/* 模板配置 */}
|
||||
<h2 className="font-medium text-gray-900 pt-6 mb-2 text-lg">
|
||||
模板配置
|
||||
</h2>
|
||||
{/* <Form.Item label="模板类型">
|
||||
<Radio.Group
|
||||
value={templateType}
|
||||
onChange={(e) => setTemplateType(e.target.value)}
|
||||
>
|
||||
<Radio value="default">使用默认模板</Radio>
|
||||
<Radio value="custom">自定义DataX JSON配置</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item> */}
|
||||
{templateType === "default" && (
|
||||
<>
|
||||
{
|
||||
<Form.Item label="选择模板">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
|
||||
{defaultTemplates.map((template) => (
|
||||
<div
|
||||
key={template.id}
|
||||
className={`border p-4 rounded-md hover:shadow-lg transition-shadow ${
|
||||
selectedTemplate === template.id
|
||||
? "border-blue-500"
|
||||
: "border-gray-300"
|
||||
}`}
|
||||
onClick={() => {
|
||||
setSelectedTemplate(template.id as TemplateType);
|
||||
// 使用函数式更新,合并之前的 config
|
||||
setNewTask((prev: any) => ({
|
||||
...prev,
|
||||
config: {
|
||||
templateType: template.id,
|
||||
},
|
||||
}));
|
||||
// 同步表单显示
|
||||
form.setFieldsValue({
|
||||
config: { templateType: template.id },
|
||||
});
|
||||
}}
|
||||
>
|
||||
<div className="font-medium">{template.name}</div>
|
||||
<div className="text-gray-500">
|
||||
{template.description}
|
||||
</div>
|
||||
<div className="text-gray-400">
|
||||
{template.config.reader} → {template.config.writer}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Form.Item>
|
||||
}
|
||||
{/* nas import */}
|
||||
{selectedTemplate === TemplateType.NAS && (
|
||||
<div className="grid grid-cols-2 gap-3 px-2 bg-blue-50 rounded">
|
||||
<Form.Item
|
||||
name={["config", "ip"]}
|
||||
rules={[{ required: true, message: "请输入NAS地址" }]}
|
||||
label="NAS地址"
|
||||
>
|
||||
<Input placeholder="192.168.1.100" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={["config", "path"]}
|
||||
rules={[{ required: true, message: "请输入共享路径" }]}
|
||||
label="共享路径"
|
||||
>
|
||||
<Input placeholder="/share/importConfig" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={["config", "files"]}
|
||||
label="文件列表"
|
||||
className="col-span-2"
|
||||
>
|
||||
<Select placeholder="请选择文件列表" mode="tags" />
|
||||
</Form.Item>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* obs import */}
|
||||
{selectedTemplate === TemplateType.OBS && (
|
||||
<div className="grid grid-cols-2 gap-3 p-4 bg-blue-50 rounded-lg">
|
||||
<Form.Item
|
||||
name={["config", "endpoint"]}
|
||||
rules={[{ required: true }]}
|
||||
label="Endpoint"
|
||||
>
|
||||
<Input
|
||||
className="h-8 text-xs"
|
||||
placeholder="obs.cn-north-4.myhuaweicloud.com"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={["config", "bucket"]}
|
||||
rules={[{ required: true }]}
|
||||
label="Bucket"
|
||||
>
|
||||
<Input className="h-8 text-xs" placeholder="my-bucket" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={["config", "accessKey"]}
|
||||
rules={[{ required: true }]}
|
||||
label="Access Key"
|
||||
>
|
||||
<Input className="h-8 text-xs" placeholder="Access Key" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={["config", "secretKey"]}
|
||||
rules={[{ required: true }]}
|
||||
label="Secret Key"
|
||||
>
|
||||
<Input
|
||||
type="password"
|
||||
className="h-8 text-xs"
|
||||
placeholder="Secret Key"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={["config", "prefix"]}
|
||||
rules={[{ required: true }]}
|
||||
label="Prefix"
|
||||
>
|
||||
<Input className="h-8 text-xs" placeholder="Prefix" />
|
||||
</Form.Item>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* mysql import */}
|
||||
{selectedTemplate === TemplateType.MYSQL && (
|
||||
<div className="grid grid-cols-2 gap-3 px-2 bg-blue-50 rounded">
|
||||
<Form.Item
|
||||
name={["config", "jdbcUrl"]}
|
||||
rules={[{ required: true, message: "请输入数据库链接" }]}
|
||||
label="数据库链接"
|
||||
className="col-span-2"
|
||||
>
|
||||
<Input placeholder="jdbc:mysql://localhost:3306/mysql?useUnicode=true&characterEncoding=utf8" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={["config", "username"]}
|
||||
rules={[{ required: true, message: "请输入用户名" }]}
|
||||
label="用户名"
|
||||
>
|
||||
<Input placeholder="mysql" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={["config", "password"]}
|
||||
rules={[{ required: true, message: "请输入密码" }]}
|
||||
label="密码"
|
||||
>
|
||||
<Input type="password" className="h-8 text-xs" placeholder="Secret Key" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={["config", "querySql"]}
|
||||
rules={[{ required: true, message: "请输入查询语句" }]}
|
||||
label="查询语句"
|
||||
>
|
||||
<Input placeholder="select * from your_table" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={["config", "headers"]}
|
||||
label="列名"
|
||||
className="col-span-2"
|
||||
>
|
||||
<Select placeholder="请输入列名" mode="tags" />
|
||||
</Form.Item>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{templateType === "custom" && (
|
||||
<Form.Item label="DataX JSON配置">
|
||||
<TextArea
|
||||
placeholder="请输入DataX JSON配置..."
|
||||
value={customConfig}
|
||||
onChange={(e) => setCustomConfig(e.target.value)}
|
||||
rows={12}
|
||||
className="w-full"
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
{/* 数据集配置 */}
|
||||
{templateType === "default" && (
|
||||
<>
|
||||
<h2 className="font-medium text-gray-900 my-4 text-lg">
|
||||
数据集配置
|
||||
</h2>
|
||||
<Form.Item
|
||||
label="是否创建数据集"
|
||||
name="createDataset"
|
||||
required
|
||||
rules={[{ required: true, message: "请选择是否创建数据集" }]}
|
||||
tooltip={"支持后续在【数据管理】中手动创建数据集并关联至此任务。"}
|
||||
>
|
||||
<Radio.Group
|
||||
value={isCreateDataset}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
let datasetInit = null;
|
||||
if (value === true) {
|
||||
datasetInit = {};
|
||||
}
|
||||
form.setFieldsValue({
|
||||
dataset: datasetInit,
|
||||
});
|
||||
setNewTask((prev: any) => ({
|
||||
...prev,
|
||||
dataset: datasetInit,
|
||||
}));
|
||||
setIsCreateDataset(e.target.value);
|
||||
}}
|
||||
>
|
||||
<Radio value={true}>是</Radio>
|
||||
<Radio value={false}>否</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
{isCreateDataset && (
|
||||
<>
|
||||
<Form.Item
|
||||
label="数据集名称"
|
||||
name={["dataset", "name"]}
|
||||
required
|
||||
>
|
||||
<Input
|
||||
placeholder="输入数据集名称"
|
||||
onChange={(e) => {
|
||||
setNewTask((prev: any) => ({
|
||||
...prev,
|
||||
dataset: {
|
||||
...(prev.dataset || {}),
|
||||
name: e.target.value,
|
||||
},
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="数据集类型"
|
||||
name={["dataset", "datasetType"]}
|
||||
rules={[{ required: true, message: "请选择数据集类型" }]}
|
||||
>
|
||||
<RadioCard
|
||||
options={datasetTypes}
|
||||
value={newTask.dataset?.datasetType}
|
||||
onChange={(type) => {
|
||||
form.setFieldValue(["dataset", "datasetType"], type);
|
||||
setNewTask((prev: any) => ({
|
||||
...prev,
|
||||
dataset: {
|
||||
...(prev.dataset || {}),
|
||||
datasetType: type as DatasetSubType,
|
||||
},
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Form>
|
||||
</div>
|
||||
<div className="flex gap-2 justify-end border-top p-6">
|
||||
<Button onClick={() => navigate("/data/collection")}>取消</Button>
|
||||
<Button type="primary" onClick={handleSubmit}>
|
||||
创建任务
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
import { useState } from "react";
|
||||
import { Input, Button, Radio, Form, App, Select } from "antd";
|
||||
import { Link, useNavigate } from "react-router";
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
import { createTaskUsingPost } from "../collection.apis";
|
||||
import SimpleCronScheduler from "@/pages/DataCollection/Create/SimpleCronScheduler";
|
||||
import RadioCard from "@/components/RadioCard";
|
||||
import { datasetTypes } from "@/pages/DataManagement/dataset.const";
|
||||
import { SyncModeMap } from "../collection.const";
|
||||
import { SyncMode } from "../collection.model";
|
||||
import { DatasetSubType } from "@/pages/DataManagement/dataset.model";
|
||||
|
||||
const { TextArea } = Input;
|
||||
|
||||
const defaultTemplates = [
|
||||
{
|
||||
id: "NAS",
|
||||
name: "NAS到本地",
|
||||
description: "从NAS文件系统导入数据到本地文件系统",
|
||||
config: {
|
||||
reader: "nfsreader",
|
||||
writer: "localwriter",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "OBS",
|
||||
name: "OBS到本地",
|
||||
description: "从OBS文件系统导入数据到本地文件系统",
|
||||
config: {
|
||||
reader: "obsreader",
|
||||
writer: "localwriter",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "MYSQL",
|
||||
name: "Mysql到本地",
|
||||
description: "从Mysql中导入数据到本地文件系统",
|
||||
config: {
|
||||
reader: "mysqlreader",
|
||||
writer: "localwriter",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const syncModeOptions = Object.values(SyncModeMap);
|
||||
|
||||
enum TemplateType {
|
||||
NAS = "NAS",
|
||||
OBS = "OBS",
|
||||
MYSQL = "MYSQL",
|
||||
}
|
||||
|
||||
export default function CollectionTaskCreate() {
|
||||
const navigate = useNavigate();
|
||||
const [form] = Form.useForm();
|
||||
const { message } = App.useApp();
|
||||
|
||||
const [templateType, setTemplateType] = useState<"default" | "custom">(
|
||||
"default"
|
||||
);
|
||||
// 默认模板类型设为 NAS
|
||||
const [selectedTemplate, setSelectedTemplate] = useState<TemplateType>(
|
||||
TemplateType.NAS
|
||||
);
|
||||
const [customConfig, setCustomConfig] = useState("");
|
||||
|
||||
// 将 newTask 设为 any,并初始化 config.templateType 为 NAS
|
||||
const [newTask, setNewTask] = useState<any>({
|
||||
name: "",
|
||||
description: "",
|
||||
syncMode: SyncMode.ONCE,
|
||||
cronExpression: "",
|
||||
maxRetries: 10,
|
||||
dataset: null,
|
||||
config: { templateType: TemplateType.NAS },
|
||||
createDataset: false,
|
||||
});
|
||||
const [scheduleExpression, setScheduleExpression] = useState({
|
||||
type: "once",
|
||||
time: "00:00",
|
||||
cronExpression: "0 0 0 * * ?",
|
||||
});
|
||||
|
||||
const [isCreateDataset, setIsCreateDataset] = useState(false);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
await form.validateFields();
|
||||
if (templateType === "default" && !selectedTemplate) {
|
||||
window.alert("请选择默认模板");
|
||||
return;
|
||||
}
|
||||
if (templateType === "custom" && !customConfig.trim()) {
|
||||
window.alert("请填写自定义配置");
|
||||
return;
|
||||
}
|
||||
|
||||
// 构建最终 payload,不依赖异步 setState
|
||||
const payload = {
|
||||
...newTask,
|
||||
taskType:
|
||||
templateType === "default" ? selectedTemplate : "CUSTOM",
|
||||
config: {
|
||||
...((newTask && newTask.config) || {}),
|
||||
...(templateType === "custom" ? { dataxJson: customConfig } : {}),
|
||||
},
|
||||
};
|
||||
|
||||
console.log("创建任务 payload:", payload);
|
||||
|
||||
await createTaskUsingPost(payload);
|
||||
message.success("任务创建成功");
|
||||
navigate("/data/collection");
|
||||
} catch (error) {
|
||||
message.error(`${error?.data?.message}:${error?.data?.data}`);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center">
|
||||
<Link to="/data/collection">
|
||||
<Button type="text">
|
||||
<ArrowLeft className="w-4 h-4 mr-1" />
|
||||
</Button>
|
||||
</Link>
|
||||
<h1 className="text-xl font-bold bg-clip-text">创建归集任务</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-overflow-auto border-card">
|
||||
<div className="flex-1 overflow-auto p-6">
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
initialValues={newTask}
|
||||
onValuesChange={(_, allValues) => {
|
||||
setNewTask({ ...newTask, ...allValues });
|
||||
}}
|
||||
>
|
||||
{/* 基本信息 */}
|
||||
<h2 className="font-medium text-gray-900 text-lg mb-2">基本信息</h2>
|
||||
|
||||
<Form.Item
|
||||
label="名称"
|
||||
name="name"
|
||||
rules={[{ required: true, message: "请输入任务名称" }]}
|
||||
>
|
||||
<Input placeholder="请输入任务名称" />
|
||||
</Form.Item>
|
||||
<Form.Item label="描述" name="description">
|
||||
<TextArea placeholder="请输入任务描述" rows={3} />
|
||||
</Form.Item>
|
||||
|
||||
{/* 同步配置 */}
|
||||
<h2 className="font-medium text-gray-900 pt-6 mb-2 text-lg">
|
||||
同步配置
|
||||
</h2>
|
||||
<Form.Item name="syncMode" label="同步方式">
|
||||
<Radio.Group
|
||||
value={newTask.syncMode}
|
||||
options={syncModeOptions}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
setNewTask({
|
||||
...newTask,
|
||||
syncMode: value,
|
||||
scheduleExpression:
|
||||
value === SyncMode.SCHEDULED
|
||||
? scheduleExpression.cronExpression
|
||||
: "",
|
||||
});
|
||||
}}
|
||||
></Radio.Group>
|
||||
</Form.Item>
|
||||
{newTask.syncMode === SyncMode.SCHEDULED && (
|
||||
<Form.Item
|
||||
label=""
|
||||
rules={[{ required: true, message: "请输入Cron表达式" }]}
|
||||
>
|
||||
<SimpleCronScheduler
|
||||
className="px-2 rounded"
|
||||
value={scheduleExpression}
|
||||
onChange={(value) => {
|
||||
setScheduleExpression(value);
|
||||
setNewTask({
|
||||
...newTask,
|
||||
scheduleExpression: value.cronExpression,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
{/* 模板配置 */}
|
||||
<h2 className="font-medium text-gray-900 pt-6 mb-2 text-lg">
|
||||
模板配置
|
||||
</h2>
|
||||
{/* <Form.Item label="模板类型">
|
||||
<Radio.Group
|
||||
value={templateType}
|
||||
onChange={(e) => setTemplateType(e.target.value)}
|
||||
>
|
||||
<Radio value="default">使用默认模板</Radio>
|
||||
<Radio value="custom">自定义DataX JSON配置</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item> */}
|
||||
{templateType === "default" && (
|
||||
<>
|
||||
{
|
||||
<Form.Item label="选择模板">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
|
||||
{defaultTemplates.map((template) => (
|
||||
<div
|
||||
key={template.id}
|
||||
className={`border p-4 rounded-md hover:shadow-lg transition-shadow ${
|
||||
selectedTemplate === template.id
|
||||
? "border-blue-500"
|
||||
: "border-gray-300"
|
||||
}`}
|
||||
onClick={() => {
|
||||
setSelectedTemplate(template.id as TemplateType);
|
||||
// 使用函数式更新,合并之前的 config
|
||||
setNewTask((prev: any) => ({
|
||||
...prev,
|
||||
config: {
|
||||
templateType: template.id,
|
||||
},
|
||||
}));
|
||||
// 同步表单显示
|
||||
form.setFieldsValue({
|
||||
config: { templateType: template.id },
|
||||
});
|
||||
}}
|
||||
>
|
||||
<div className="font-medium">{template.name}</div>
|
||||
<div className="text-gray-500">
|
||||
{template.description}
|
||||
</div>
|
||||
<div className="text-gray-400">
|
||||
{template.config.reader} → {template.config.writer}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Form.Item>
|
||||
}
|
||||
{/* nas import */}
|
||||
{selectedTemplate === TemplateType.NAS && (
|
||||
<div className="grid grid-cols-2 gap-3 px-2 bg-blue-50 rounded">
|
||||
<Form.Item
|
||||
name={["config", "ip"]}
|
||||
rules={[{ required: true, message: "请输入NAS地址" }]}
|
||||
label="NAS地址"
|
||||
>
|
||||
<Input placeholder="192.168.1.100" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={["config", "path"]}
|
||||
rules={[{ required: true, message: "请输入共享路径" }]}
|
||||
label="共享路径"
|
||||
>
|
||||
<Input placeholder="/share/importConfig" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={["config", "files"]}
|
||||
label="文件列表"
|
||||
className="col-span-2"
|
||||
>
|
||||
<Select placeholder="请选择文件列表" mode="tags" />
|
||||
</Form.Item>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* obs import */}
|
||||
{selectedTemplate === TemplateType.OBS && (
|
||||
<div className="grid grid-cols-2 gap-3 p-4 bg-blue-50 rounded-lg">
|
||||
<Form.Item
|
||||
name={["config", "endpoint"]}
|
||||
rules={[{ required: true }]}
|
||||
label="Endpoint"
|
||||
>
|
||||
<Input
|
||||
className="h-8 text-xs"
|
||||
placeholder="obs.cn-north-4.myhuaweicloud.com"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={["config", "bucket"]}
|
||||
rules={[{ required: true }]}
|
||||
label="Bucket"
|
||||
>
|
||||
<Input className="h-8 text-xs" placeholder="my-bucket" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={["config", "accessKey"]}
|
||||
rules={[{ required: true }]}
|
||||
label="Access Key"
|
||||
>
|
||||
<Input className="h-8 text-xs" placeholder="Access Key" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={["config", "secretKey"]}
|
||||
rules={[{ required: true }]}
|
||||
label="Secret Key"
|
||||
>
|
||||
<Input
|
||||
type="password"
|
||||
className="h-8 text-xs"
|
||||
placeholder="Secret Key"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={["config", "prefix"]}
|
||||
rules={[{ required: true }]}
|
||||
label="Prefix"
|
||||
>
|
||||
<Input className="h-8 text-xs" placeholder="Prefix" />
|
||||
</Form.Item>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* mysql import */}
|
||||
{selectedTemplate === TemplateType.MYSQL && (
|
||||
<div className="grid grid-cols-2 gap-3 px-2 bg-blue-50 rounded">
|
||||
<Form.Item
|
||||
name={["config", "jdbcUrl"]}
|
||||
rules={[{ required: true, message: "请输入数据库链接" }]}
|
||||
label="数据库链接"
|
||||
className="col-span-2"
|
||||
>
|
||||
<Input placeholder="jdbc:mysql://localhost:3306/mysql?useUnicode=true&characterEncoding=utf8" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={["config", "username"]}
|
||||
rules={[{ required: true, message: "请输入用户名" }]}
|
||||
label="用户名"
|
||||
>
|
||||
<Input placeholder="mysql" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={["config", "password"]}
|
||||
rules={[{ required: true, message: "请输入密码" }]}
|
||||
label="密码"
|
||||
>
|
||||
<Input type="password" className="h-8 text-xs" placeholder="Secret Key" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={["config", "querySql"]}
|
||||
rules={[{ required: true, message: "请输入查询语句" }]}
|
||||
label="查询语句"
|
||||
>
|
||||
<Input placeholder="select * from your_table" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={["config", "headers"]}
|
||||
label="列名"
|
||||
className="col-span-2"
|
||||
>
|
||||
<Select placeholder="请输入列名" mode="tags" />
|
||||
</Form.Item>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{templateType === "custom" && (
|
||||
<Form.Item label="DataX JSON配置">
|
||||
<TextArea
|
||||
placeholder="请输入DataX JSON配置..."
|
||||
value={customConfig}
|
||||
onChange={(e) => setCustomConfig(e.target.value)}
|
||||
rows={12}
|
||||
className="w-full"
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
{/* 数据集配置 */}
|
||||
{templateType === "default" && (
|
||||
<>
|
||||
<h2 className="font-medium text-gray-900 my-4 text-lg">
|
||||
数据集配置
|
||||
</h2>
|
||||
<Form.Item
|
||||
label="是否创建数据集"
|
||||
name="createDataset"
|
||||
required
|
||||
rules={[{ required: true, message: "请选择是否创建数据集" }]}
|
||||
tooltip={"支持后续在【数据管理】中手动创建数据集并关联至此任务。"}
|
||||
>
|
||||
<Radio.Group
|
||||
value={isCreateDataset}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
let datasetInit = null;
|
||||
if (value === true) {
|
||||
datasetInit = {};
|
||||
}
|
||||
form.setFieldsValue({
|
||||
dataset: datasetInit,
|
||||
});
|
||||
setNewTask((prev: any) => ({
|
||||
...prev,
|
||||
dataset: datasetInit,
|
||||
}));
|
||||
setIsCreateDataset(e.target.value);
|
||||
}}
|
||||
>
|
||||
<Radio value={true}>是</Radio>
|
||||
<Radio value={false}>否</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
{isCreateDataset && (
|
||||
<>
|
||||
<Form.Item
|
||||
label="数据集名称"
|
||||
name={["dataset", "name"]}
|
||||
required
|
||||
>
|
||||
<Input
|
||||
placeholder="输入数据集名称"
|
||||
onChange={(e) => {
|
||||
setNewTask((prev: any) => ({
|
||||
...prev,
|
||||
dataset: {
|
||||
...(prev.dataset || {}),
|
||||
name: e.target.value,
|
||||
},
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="数据集类型"
|
||||
name={["dataset", "datasetType"]}
|
||||
rules={[{ required: true, message: "请选择数据集类型" }]}
|
||||
>
|
||||
<RadioCard
|
||||
options={datasetTypes}
|
||||
value={newTask.dataset?.datasetType}
|
||||
onChange={(type) => {
|
||||
form.setFieldValue(["dataset", "datasetType"], type);
|
||||
setNewTask((prev: any) => ({
|
||||
...prev,
|
||||
dataset: {
|
||||
...(prev.dataset || {}),
|
||||
datasetType: type as DatasetSubType,
|
||||
},
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Form>
|
||||
</div>
|
||||
<div className="flex gap-2 justify-end border-top p-6">
|
||||
<Button onClick={() => navigate("/data/collection")}>取消</Button>
|
||||
<Button type="primary" onClick={handleSubmit}>
|
||||
创建任务
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,209 +1,209 @@
|
||||
import React, { useState, useCallback } from "react";
|
||||
import {
|
||||
Card,
|
||||
Radio,
|
||||
Select,
|
||||
Space,
|
||||
Typography,
|
||||
TimePicker,
|
||||
Button,
|
||||
Input,
|
||||
Form,
|
||||
} from "antd";
|
||||
import type { RadioChangeEvent } from "antd";
|
||||
import type { Dayjs } from "dayjs";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
const { Text } = Typography;
|
||||
const { Option } = Select;
|
||||
|
||||
export interface SimpleCronConfig {
|
||||
type: "once" | "daily" | "weekly" | "monthly";
|
||||
time?: string; // HH:mm 格式
|
||||
weekDay?: number; // 0-6, 0 表示周日
|
||||
monthDay?: number; // 1-31
|
||||
cronExpression: string;
|
||||
}
|
||||
|
||||
interface SimpleCronSchedulerProps {
|
||||
value?: SimpleCronConfig;
|
||||
onChange?: (config: SimpleCronConfig) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const defaultConfig: SimpleCronConfig = {
|
||||
type: "once",
|
||||
time: "00:00",
|
||||
cronExpression: "0 0 0 * * ?",
|
||||
};
|
||||
|
||||
// 生成周几选项
|
||||
const weekDayOptions = [
|
||||
{ label: "周日", value: 0 },
|
||||
{ label: "周一", value: 1 },
|
||||
{ label: "周二", value: 2 },
|
||||
{ label: "周三", value: 3 },
|
||||
{ label: "周四", value: 4 },
|
||||
{ label: "周五", value: 5 },
|
||||
{ label: "周六", value: 6 },
|
||||
];
|
||||
|
||||
// 生成月份日期选项
|
||||
const monthDayOptions = Array.from({ length: 31 }, (_, i) => ({
|
||||
label: `${i + 1}日`,
|
||||
value: i + 1,
|
||||
}));
|
||||
|
||||
// 常用时间预设
|
||||
const commonTimePresets = [
|
||||
{ label: "上午 9:00", value: "09:00" },
|
||||
{ label: "中午 12:00", value: "12:00" },
|
||||
{ label: "下午 2:00", value: "14:00" },
|
||||
{ label: "下午 6:00", value: "18:00" },
|
||||
{ label: "晚上 8:00", value: "20:00" },
|
||||
{ label: "午夜 0:00", value: "00:00" },
|
||||
];
|
||||
|
||||
const SimpleCronScheduler: React.FC<SimpleCronSchedulerProps> = ({
|
||||
value = defaultConfig,
|
||||
onChange,
|
||||
className,
|
||||
}) => {
|
||||
const [config, setConfig] = useState<SimpleCronConfig>(value);
|
||||
|
||||
// 更新配置并生成 cron 表达式
|
||||
const updateConfig = useCallback(
|
||||
(updates: Partial<SimpleCronConfig>) => {
|
||||
const newConfig = { ...config, ...updates };
|
||||
const [hour, minute] = (newConfig.time || "00:00").split(":");
|
||||
|
||||
// 根据不同类型生成 cron 表达式
|
||||
let cronExpression = "";
|
||||
switch (newConfig.type) {
|
||||
case "once":
|
||||
cronExpression = `0 ${minute} ${hour} * * ?`;
|
||||
break;
|
||||
case "daily":
|
||||
cronExpression = `0 ${minute} ${hour} * * ?`;
|
||||
break;
|
||||
case "weekly":
|
||||
cronExpression = `0 ${minute} ${hour} ? * ${newConfig.weekDay}`;
|
||||
break;
|
||||
case "monthly":
|
||||
cronExpression = `0 ${minute} ${hour} ${newConfig.monthDay} * ?`;
|
||||
break;
|
||||
}
|
||||
|
||||
newConfig.cronExpression = cronExpression;
|
||||
setConfig(newConfig);
|
||||
onChange?.(newConfig);
|
||||
},
|
||||
[config, onChange]
|
||||
);
|
||||
|
||||
// 处理类型改变
|
||||
const handleTypeChange = (type) => {
|
||||
const updates: Partial<SimpleCronConfig> = { type };
|
||||
|
||||
// 设置默认值
|
||||
if (type === "weekly" && !config.weekDay) {
|
||||
updates.weekDay = 1; // 默认周一
|
||||
} else if (type === "monthly" && !config.monthDay) {
|
||||
updates.monthDay = 1; // 默认每月1号
|
||||
}
|
||||
|
||||
updateConfig(updates);
|
||||
};
|
||||
|
||||
// 处理时间改变
|
||||
const handleTimeChange = (value: Dayjs | null) => {
|
||||
if (value) {
|
||||
updateConfig({ time: value.format("HH:mm") });
|
||||
}
|
||||
};
|
||||
|
||||
// 快速设置预设时间
|
||||
const handleTimePreset = (time: string) => {
|
||||
updateConfig({ time });
|
||||
};
|
||||
|
||||
return (
|
||||
<Space direction="vertical" className={`w-full ${className || ""}`}>
|
||||
{/* 执行周期选择 */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Form.Item label="执行周期" required>
|
||||
<Select value={config.type} onChange={handleTypeChange}>
|
||||
<Select.Option value="once">仅执行一次</Select.Option>
|
||||
<Select.Option value="daily">每天执行</Select.Option>
|
||||
<Select.Option value="weekly">每周执行</Select.Option>
|
||||
<Select.Option value="monthly">每月执行</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
{/* 周几选择 */}
|
||||
{config.type === "weekly" && (
|
||||
<Form.Item label="执行日期" required>
|
||||
<Select
|
||||
className="w-32"
|
||||
value={config.weekDay}
|
||||
onChange={(weekDay) => updateConfig({ weekDay })}
|
||||
placeholder="选择周几"
|
||||
options={weekDayOptions}
|
||||
></Select>
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
{/* 月份日期选择 */}
|
||||
{config.type === "monthly" && (
|
||||
<Form.Item label="执行日期" required>
|
||||
<Select
|
||||
className="w-32"
|
||||
value={config.monthDay}
|
||||
onChange={(monthDay) => updateConfig({ monthDay })}
|
||||
placeholder="选择日期"
|
||||
options={monthDayOptions}
|
||||
></Select>
|
||||
</Form.Item>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 时间选择 */}
|
||||
<Form.Item label="执行时间" required>
|
||||
<Space wrap>
|
||||
<TimePicker
|
||||
format="HH:mm"
|
||||
value={config.time ? dayjs(config.time, "HH:mm") : null}
|
||||
onChange={handleTimeChange}
|
||||
placeholder="选择时间"
|
||||
/>
|
||||
<Space wrap className="mt-2">
|
||||
{commonTimePresets.map((preset) => (
|
||||
<Button
|
||||
key={preset.value}
|
||||
size="small"
|
||||
className={
|
||||
config.time === preset.value ? "ant-btn-primary" : ""
|
||||
}
|
||||
onClick={() => handleTimePreset(preset.value)}
|
||||
>
|
||||
{preset.label}
|
||||
</Button>
|
||||
))}
|
||||
</Space>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
|
||||
{/* Cron 表达式预览 */}
|
||||
{/* <div className="mt-4 pt-4 border-t border-gray-200">
|
||||
<Text>生成的 Cron 表达式</Text>
|
||||
<Input
|
||||
className="mt-2 bg-gray-100"
|
||||
value={config.cronExpression}
|
||||
readOnly
|
||||
/>
|
||||
</div> */}
|
||||
</Space>
|
||||
);
|
||||
};
|
||||
|
||||
export default SimpleCronScheduler;
|
||||
import React, { useState, useCallback } from "react";
|
||||
import {
|
||||
Card,
|
||||
Radio,
|
||||
Select,
|
||||
Space,
|
||||
Typography,
|
||||
TimePicker,
|
||||
Button,
|
||||
Input,
|
||||
Form,
|
||||
} from "antd";
|
||||
import type { RadioChangeEvent } from "antd";
|
||||
import type { Dayjs } from "dayjs";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
const { Text } = Typography;
|
||||
const { Option } = Select;
|
||||
|
||||
export interface SimpleCronConfig {
|
||||
type: "once" | "daily" | "weekly" | "monthly";
|
||||
time?: string; // HH:mm 格式
|
||||
weekDay?: number; // 0-6, 0 表示周日
|
||||
monthDay?: number; // 1-31
|
||||
cronExpression: string;
|
||||
}
|
||||
|
||||
interface SimpleCronSchedulerProps {
|
||||
value?: SimpleCronConfig;
|
||||
onChange?: (config: SimpleCronConfig) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const defaultConfig: SimpleCronConfig = {
|
||||
type: "once",
|
||||
time: "00:00",
|
||||
cronExpression: "0 0 0 * * ?",
|
||||
};
|
||||
|
||||
// 生成周几选项
|
||||
const weekDayOptions = [
|
||||
{ label: "周日", value: 0 },
|
||||
{ label: "周一", value: 1 },
|
||||
{ label: "周二", value: 2 },
|
||||
{ label: "周三", value: 3 },
|
||||
{ label: "周四", value: 4 },
|
||||
{ label: "周五", value: 5 },
|
||||
{ label: "周六", value: 6 },
|
||||
];
|
||||
|
||||
// 生成月份日期选项
|
||||
const monthDayOptions = Array.from({ length: 31 }, (_, i) => ({
|
||||
label: `${i + 1}日`,
|
||||
value: i + 1,
|
||||
}));
|
||||
|
||||
// 常用时间预设
|
||||
const commonTimePresets = [
|
||||
{ label: "上午 9:00", value: "09:00" },
|
||||
{ label: "中午 12:00", value: "12:00" },
|
||||
{ label: "下午 2:00", value: "14:00" },
|
||||
{ label: "下午 6:00", value: "18:00" },
|
||||
{ label: "晚上 8:00", value: "20:00" },
|
||||
{ label: "午夜 0:00", value: "00:00" },
|
||||
];
|
||||
|
||||
const SimpleCronScheduler: React.FC<SimpleCronSchedulerProps> = ({
|
||||
value = defaultConfig,
|
||||
onChange,
|
||||
className,
|
||||
}) => {
|
||||
const [config, setConfig] = useState<SimpleCronConfig>(value);
|
||||
|
||||
// 更新配置并生成 cron 表达式
|
||||
const updateConfig = useCallback(
|
||||
(updates: Partial<SimpleCronConfig>) => {
|
||||
const newConfig = { ...config, ...updates };
|
||||
const [hour, minute] = (newConfig.time || "00:00").split(":");
|
||||
|
||||
// 根据不同类型生成 cron 表达式
|
||||
let cronExpression = "";
|
||||
switch (newConfig.type) {
|
||||
case "once":
|
||||
cronExpression = `0 ${minute} ${hour} * * ?`;
|
||||
break;
|
||||
case "daily":
|
||||
cronExpression = `0 ${minute} ${hour} * * ?`;
|
||||
break;
|
||||
case "weekly":
|
||||
cronExpression = `0 ${minute} ${hour} ? * ${newConfig.weekDay}`;
|
||||
break;
|
||||
case "monthly":
|
||||
cronExpression = `0 ${minute} ${hour} ${newConfig.monthDay} * ?`;
|
||||
break;
|
||||
}
|
||||
|
||||
newConfig.cronExpression = cronExpression;
|
||||
setConfig(newConfig);
|
||||
onChange?.(newConfig);
|
||||
},
|
||||
[config, onChange]
|
||||
);
|
||||
|
||||
// 处理类型改变
|
||||
const handleTypeChange = (type) => {
|
||||
const updates: Partial<SimpleCronConfig> = { type };
|
||||
|
||||
// 设置默认值
|
||||
if (type === "weekly" && !config.weekDay) {
|
||||
updates.weekDay = 1; // 默认周一
|
||||
} else if (type === "monthly" && !config.monthDay) {
|
||||
updates.monthDay = 1; // 默认每月1号
|
||||
}
|
||||
|
||||
updateConfig(updates);
|
||||
};
|
||||
|
||||
// 处理时间改变
|
||||
const handleTimeChange = (value: Dayjs | null) => {
|
||||
if (value) {
|
||||
updateConfig({ time: value.format("HH:mm") });
|
||||
}
|
||||
};
|
||||
|
||||
// 快速设置预设时间
|
||||
const handleTimePreset = (time: string) => {
|
||||
updateConfig({ time });
|
||||
};
|
||||
|
||||
return (
|
||||
<Space direction="vertical" className={`w-full ${className || ""}`}>
|
||||
{/* 执行周期选择 */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Form.Item label="执行周期" required>
|
||||
<Select value={config.type} onChange={handleTypeChange}>
|
||||
<Select.Option value="once">仅执行一次</Select.Option>
|
||||
<Select.Option value="daily">每天执行</Select.Option>
|
||||
<Select.Option value="weekly">每周执行</Select.Option>
|
||||
<Select.Option value="monthly">每月执行</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
{/* 周几选择 */}
|
||||
{config.type === "weekly" && (
|
||||
<Form.Item label="执行日期" required>
|
||||
<Select
|
||||
className="w-32"
|
||||
value={config.weekDay}
|
||||
onChange={(weekDay) => updateConfig({ weekDay })}
|
||||
placeholder="选择周几"
|
||||
options={weekDayOptions}
|
||||
></Select>
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
{/* 月份日期选择 */}
|
||||
{config.type === "monthly" && (
|
||||
<Form.Item label="执行日期" required>
|
||||
<Select
|
||||
className="w-32"
|
||||
value={config.monthDay}
|
||||
onChange={(monthDay) => updateConfig({ monthDay })}
|
||||
placeholder="选择日期"
|
||||
options={monthDayOptions}
|
||||
></Select>
|
||||
</Form.Item>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 时间选择 */}
|
||||
<Form.Item label="执行时间" required>
|
||||
<Space wrap>
|
||||
<TimePicker
|
||||
format="HH:mm"
|
||||
value={config.time ? dayjs(config.time, "HH:mm") : null}
|
||||
onChange={handleTimeChange}
|
||||
placeholder="选择时间"
|
||||
/>
|
||||
<Space wrap className="mt-2">
|
||||
{commonTimePresets.map((preset) => (
|
||||
<Button
|
||||
key={preset.value}
|
||||
size="small"
|
||||
className={
|
||||
config.time === preset.value ? "ant-btn-primary" : ""
|
||||
}
|
||||
onClick={() => handleTimePreset(preset.value)}
|
||||
>
|
||||
{preset.label}
|
||||
</Button>
|
||||
))}
|
||||
</Space>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
|
||||
{/* Cron 表达式预览 */}
|
||||
{/* <div className="mt-4 pt-4 border-t border-gray-200">
|
||||
<Text>生成的 Cron 表达式</Text>
|
||||
<Input
|
||||
className="mt-2 bg-gray-100"
|
||||
value={config.cronExpression}
|
||||
readOnly
|
||||
/>
|
||||
</div> */}
|
||||
</Space>
|
||||
);
|
||||
};
|
||||
|
||||
export default SimpleCronScheduler;
|
||||
|
||||
@@ -1,41 +1,41 @@
|
||||
import { useState } from "react";
|
||||
import { Button, Tabs } from "antd";
|
||||
import { PlusOutlined } from "@ant-design/icons";
|
||||
import TaskManagement from "./TaskManagement";
|
||||
import ExecutionLog from "./ExecutionLog";
|
||||
import { useNavigate } from "react-router";
|
||||
|
||||
export default function DataCollection() {
|
||||
const navigate = useNavigate();
|
||||
const [activeTab, setActiveTab] = useState("task-management");
|
||||
|
||||
return (
|
||||
<div className="gap-4 h-full flex flex-col">
|
||||
<div className="flex justify-between items-end">
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-gray-900 mb-2">数据归集</h1>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => navigate("/data/collection/create-task")}
|
||||
icon={<PlusOutlined />}
|
||||
>
|
||||
创建任务
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Tabs
|
||||
activeKey={activeTab}
|
||||
items={[
|
||||
{ label: "任务管理", key: "task-management" },
|
||||
// { label: "执行日志", key: "execution-log" },
|
||||
]}
|
||||
onChange={(tab) => {
|
||||
setActiveTab(tab);
|
||||
}}
|
||||
/>
|
||||
{activeTab === "task-management" ? <TaskManagement /> : <ExecutionLog />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
import { useState } from "react";
|
||||
import { Button, Tabs } from "antd";
|
||||
import { PlusOutlined } from "@ant-design/icons";
|
||||
import TaskManagement from "./TaskManagement";
|
||||
import ExecutionLog from "./ExecutionLog";
|
||||
import { useNavigate } from "react-router";
|
||||
|
||||
export default function DataCollection() {
|
||||
const navigate = useNavigate();
|
||||
const [activeTab, setActiveTab] = useState("task-management");
|
||||
|
||||
return (
|
||||
<div className="gap-4 h-full flex flex-col">
|
||||
<div className="flex justify-between items-end">
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-gray-900 mb-2">数据归集</h1>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => navigate("/data/collection/create-task")}
|
||||
icon={<PlusOutlined />}
|
||||
>
|
||||
创建任务
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Tabs
|
||||
activeKey={activeTab}
|
||||
items={[
|
||||
{ label: "任务管理", key: "task-management" },
|
||||
// { label: "执行日志", key: "execution-log" },
|
||||
]}
|
||||
onChange={(tab) => {
|
||||
setActiveTab(tab);
|
||||
}}
|
||||
/>
|
||||
{activeTab === "task-management" ? <TaskManagement /> : <ExecutionLog />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,149 +1,149 @@
|
||||
import { Card, Badge, Table } from "antd";
|
||||
import type { ColumnsType } from "antd/es/table";
|
||||
import { SearchControls } from "@/components/SearchControls";
|
||||
import type { CollectionLog } from "@/pages/DataCollection/collection.model";
|
||||
import { queryExecutionLogUsingPost } from "../collection.apis";
|
||||
import { LogStatusMap, LogTriggerTypeMap } from "../collection.const";
|
||||
import useFetchData from "@/hooks/useFetchData";
|
||||
|
||||
const filterOptions = [
|
||||
{
|
||||
key: "status",
|
||||
label: "状态筛选",
|
||||
options: Object.values(LogStatusMap),
|
||||
},
|
||||
{
|
||||
key: "triggerType",
|
||||
label: "触发类型",
|
||||
options: Object.values(LogTriggerTypeMap),
|
||||
},
|
||||
];
|
||||
|
||||
export default function ExecutionLog() {
|
||||
const handleReset = () => {
|
||||
setSearchParams({
|
||||
keyword: "",
|
||||
filters: {},
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
dateRange: null,
|
||||
});
|
||||
};
|
||||
|
||||
const {
|
||||
loading,
|
||||
tableData,
|
||||
pagination,
|
||||
searchParams,
|
||||
setSearchParams,
|
||||
handleFiltersChange,
|
||||
handleKeywordChange,
|
||||
} = useFetchData(queryExecutionLogUsingPost);
|
||||
|
||||
const columns: ColumnsType<CollectionLog> = [
|
||||
{
|
||||
title: "任务名称",
|
||||
dataIndex: "taskName",
|
||||
key: "taskName",
|
||||
fixed: "left",
|
||||
render: (text: string) => <span style={{ fontWeight: 500 }}>{text}</span>,
|
||||
},
|
||||
{
|
||||
title: "状态",
|
||||
dataIndex: "status",
|
||||
key: "status",
|
||||
render: (status: string) => (
|
||||
<Badge
|
||||
text={LogStatusMap[status]?.label}
|
||||
color={LogStatusMap[status]?.color}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "触发类型",
|
||||
dataIndex: "triggerType",
|
||||
key: "triggerType",
|
||||
render: (type: string) => LogTriggerTypeMap[type].label,
|
||||
},
|
||||
{
|
||||
title: "开始时间",
|
||||
dataIndex: "startTime",
|
||||
key: "startTime",
|
||||
},
|
||||
{
|
||||
title: "结束时间",
|
||||
dataIndex: "endTime",
|
||||
key: "endTime",
|
||||
},
|
||||
{
|
||||
title: "执行时长",
|
||||
dataIndex: "duration",
|
||||
key: "duration",
|
||||
},
|
||||
{
|
||||
title: "重试次数",
|
||||
dataIndex: "retryCount",
|
||||
key: "retryCount",
|
||||
},
|
||||
{
|
||||
title: "进程ID",
|
||||
dataIndex: "processId",
|
||||
key: "processId",
|
||||
render: (text: string) => (
|
||||
<span style={{ fontFamily: "monospace" }}>{text}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "错误信息",
|
||||
dataIndex: "errorMessage",
|
||||
key: "errorMessage",
|
||||
render: (msg?: string) =>
|
||||
msg ? (
|
||||
<span style={{ color: "#f5222d" }} title={msg}>
|
||||
{msg}
|
||||
</span>
|
||||
) : (
|
||||
<span style={{ color: "#bbb" }}>-</span>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
{/* Filter Controls */}
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<SearchControls
|
||||
searchTerm={searchParams.keyword}
|
||||
onSearchChange={handleKeywordChange}
|
||||
filters={filterOptions}
|
||||
onFiltersChange={handleFiltersChange}
|
||||
showViewToggle={false}
|
||||
onClearFilters={() =>
|
||||
setSearchParams((prev) => ({
|
||||
...prev,
|
||||
filters: {},
|
||||
}))
|
||||
}
|
||||
showDatePicker
|
||||
dateRange={searchParams.dateRange || [null, null]}
|
||||
onDateChange={(date) =>
|
||||
setSearchParams((prev) => ({ ...prev, dateRange: date }))
|
||||
}
|
||||
onReload={handleReset}
|
||||
searchPlaceholder="搜索任务名称、进程ID或错误信息..."
|
||||
className="flex-1"
|
||||
/>
|
||||
</div>
|
||||
<Card>
|
||||
<Table
|
||||
loading={loading}
|
||||
columns={columns}
|
||||
dataSource={tableData}
|
||||
rowKey="id"
|
||||
pagination={pagination}
|
||||
scroll={{ x: "max-content" }}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
import { Card, Badge, Table } from "antd";
|
||||
import type { ColumnsType } from "antd/es/table";
|
||||
import { SearchControls } from "@/components/SearchControls";
|
||||
import type { CollectionLog } from "@/pages/DataCollection/collection.model";
|
||||
import { queryExecutionLogUsingPost } from "../collection.apis";
|
||||
import { LogStatusMap, LogTriggerTypeMap } from "../collection.const";
|
||||
import useFetchData from "@/hooks/useFetchData";
|
||||
|
||||
const filterOptions = [
|
||||
{
|
||||
key: "status",
|
||||
label: "状态筛选",
|
||||
options: Object.values(LogStatusMap),
|
||||
},
|
||||
{
|
||||
key: "triggerType",
|
||||
label: "触发类型",
|
||||
options: Object.values(LogTriggerTypeMap),
|
||||
},
|
||||
];
|
||||
|
||||
export default function ExecutionLog() {
|
||||
const handleReset = () => {
|
||||
setSearchParams({
|
||||
keyword: "",
|
||||
filters: {},
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
dateRange: null,
|
||||
});
|
||||
};
|
||||
|
||||
const {
|
||||
loading,
|
||||
tableData,
|
||||
pagination,
|
||||
searchParams,
|
||||
setSearchParams,
|
||||
handleFiltersChange,
|
||||
handleKeywordChange,
|
||||
} = useFetchData(queryExecutionLogUsingPost);
|
||||
|
||||
const columns: ColumnsType<CollectionLog> = [
|
||||
{
|
||||
title: "任务名称",
|
||||
dataIndex: "taskName",
|
||||
key: "taskName",
|
||||
fixed: "left",
|
||||
render: (text: string) => <span style={{ fontWeight: 500 }}>{text}</span>,
|
||||
},
|
||||
{
|
||||
title: "状态",
|
||||
dataIndex: "status",
|
||||
key: "status",
|
||||
render: (status: string) => (
|
||||
<Badge
|
||||
text={LogStatusMap[status]?.label}
|
||||
color={LogStatusMap[status]?.color}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "触发类型",
|
||||
dataIndex: "triggerType",
|
||||
key: "triggerType",
|
||||
render: (type: string) => LogTriggerTypeMap[type].label,
|
||||
},
|
||||
{
|
||||
title: "开始时间",
|
||||
dataIndex: "startTime",
|
||||
key: "startTime",
|
||||
},
|
||||
{
|
||||
title: "结束时间",
|
||||
dataIndex: "endTime",
|
||||
key: "endTime",
|
||||
},
|
||||
{
|
||||
title: "执行时长",
|
||||
dataIndex: "duration",
|
||||
key: "duration",
|
||||
},
|
||||
{
|
||||
title: "重试次数",
|
||||
dataIndex: "retryCount",
|
||||
key: "retryCount",
|
||||
},
|
||||
{
|
||||
title: "进程ID",
|
||||
dataIndex: "processId",
|
||||
key: "processId",
|
||||
render: (text: string) => (
|
||||
<span style={{ fontFamily: "monospace" }}>{text}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "错误信息",
|
||||
dataIndex: "errorMessage",
|
||||
key: "errorMessage",
|
||||
render: (msg?: string) =>
|
||||
msg ? (
|
||||
<span style={{ color: "#f5222d" }} title={msg}>
|
||||
{msg}
|
||||
</span>
|
||||
) : (
|
||||
<span style={{ color: "#bbb" }}>-</span>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
{/* Filter Controls */}
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<SearchControls
|
||||
searchTerm={searchParams.keyword}
|
||||
onSearchChange={handleKeywordChange}
|
||||
filters={filterOptions}
|
||||
onFiltersChange={handleFiltersChange}
|
||||
showViewToggle={false}
|
||||
onClearFilters={() =>
|
||||
setSearchParams((prev) => ({
|
||||
...prev,
|
||||
filters: {},
|
||||
}))
|
||||
}
|
||||
showDatePicker
|
||||
dateRange={searchParams.dateRange || [null, null]}
|
||||
onDateChange={(date) =>
|
||||
setSearchParams((prev) => ({ ...prev, dateRange: date }))
|
||||
}
|
||||
onReload={handleReset}
|
||||
searchPlaceholder="搜索任务名称、进程ID或错误信息..."
|
||||
className="flex-1"
|
||||
/>
|
||||
</div>
|
||||
<Card>
|
||||
<Table
|
||||
loading={loading}
|
||||
columns={columns}
|
||||
dataSource={tableData}
|
||||
rowKey="id"
|
||||
pagination={pagination}
|
||||
scroll={{ x: "max-content" }}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,252 +1,252 @@
|
||||
import {
|
||||
Card,
|
||||
Button,
|
||||
Badge,
|
||||
Table,
|
||||
Dropdown,
|
||||
App,
|
||||
Tooltip,
|
||||
Popconfirm,
|
||||
} from "antd";
|
||||
import {
|
||||
DeleteOutlined,
|
||||
EditOutlined,
|
||||
EllipsisOutlined,
|
||||
PauseCircleOutlined,
|
||||
PauseOutlined,
|
||||
PlayCircleOutlined,
|
||||
StopOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { SearchControls } from "@/components/SearchControls";
|
||||
import {
|
||||
deleteTaskByIdUsingDelete,
|
||||
executeTaskByIdUsingPost,
|
||||
queryTasksUsingGet,
|
||||
stopTaskByIdUsingPost,
|
||||
} from "../collection.apis";
|
||||
import { TaskStatus, type CollectionTask } from "../collection.model";
|
||||
import { StatusMap, SyncModeMap } from "../collection.const";
|
||||
import useFetchData from "@/hooks/useFetchData";
|
||||
import { useNavigate } from "react-router";
|
||||
import { mapCollectionTask } from "../collection.const";
|
||||
|
||||
export default function TaskManagement() {
|
||||
const { message } = App.useApp();
|
||||
const navigate = useNavigate();
|
||||
const filters = [
|
||||
{
|
||||
key: "status",
|
||||
label: "状态筛选",
|
||||
options: [
|
||||
{ value: "all", label: "全部状态" },
|
||||
...Object.values(StatusMap),
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const {
|
||||
loading,
|
||||
tableData,
|
||||
pagination,
|
||||
searchParams,
|
||||
setSearchParams,
|
||||
fetchData,
|
||||
handleFiltersChange,
|
||||
} = useFetchData(queryTasksUsingGet, mapCollectionTask);
|
||||
|
||||
const handleStartTask = async (taskId: string) => {
|
||||
await executeTaskByIdUsingPost(taskId);
|
||||
message.success("任务启动请求已发送");
|
||||
fetchData();
|
||||
};
|
||||
|
||||
const handleStopTask = async (taskId: string) => {
|
||||
await stopTaskByIdUsingPost(taskId);
|
||||
message.success("任务停止请求已发送");
|
||||
fetchData();
|
||||
};
|
||||
|
||||
const handleDeleteTask = async (taskId: string) => {
|
||||
await deleteTaskByIdUsingDelete(taskId);
|
||||
message.success("任务已删除");
|
||||
fetchData();
|
||||
};
|
||||
|
||||
const taskOperations = (record: CollectionTask) => {
|
||||
const isStopped = record.status === TaskStatus.STOPPED;
|
||||
const startButton = {
|
||||
key: "start",
|
||||
label: "启动",
|
||||
icon: <PlayCircleOutlined />,
|
||||
onClick: () => handleStartTask(record.id),
|
||||
};
|
||||
const stopButton = {
|
||||
key: "stop",
|
||||
label: "停止",
|
||||
icon: <PauseCircleOutlined />,
|
||||
onClick: () => handleStopTask(record.id),
|
||||
};
|
||||
const items = [
|
||||
// isStopped ? startButton : stopButton,
|
||||
// {
|
||||
// key: "edit",
|
||||
// label: "编辑",
|
||||
// icon: <EditOutlined />,
|
||||
// onClick: () => {
|
||||
// showEditTaskModal(record);
|
||||
// },
|
||||
// },
|
||||
{
|
||||
key: "delete",
|
||||
label: "删除",
|
||||
danger: true,
|
||||
icon: <DeleteOutlined />,
|
||||
confirm: {
|
||||
title: "确定要删除该任务吗?此操作不可撤销。",
|
||||
okText: "删除",
|
||||
cancelText: "取消",
|
||||
okType: "danger",
|
||||
},
|
||||
onClick: () => handleDeleteTask(record.id),
|
||||
},
|
||||
];
|
||||
return items;
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: "任务名称",
|
||||
dataIndex: "name",
|
||||
key: "name",
|
||||
fixed: "left",
|
||||
width: 150,
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: "状态",
|
||||
dataIndex: "status",
|
||||
key: "status",
|
||||
width: 150,
|
||||
ellipsis: true,
|
||||
render: (status: string) => (
|
||||
<Badge text={status.label} color={status.color} />
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "同步方式",
|
||||
dataIndex: "syncMode",
|
||||
key: "syncMode",
|
||||
width: 150,
|
||||
ellipsis: true,
|
||||
render: (text: string) => <span>{SyncModeMap[text]?.label}</span>,
|
||||
},
|
||||
{
|
||||
title: "创建时间",
|
||||
dataIndex: "createdAt",
|
||||
key: "createdAt",
|
||||
width: 150,
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: "更新时间",
|
||||
dataIndex: "updatedAt",
|
||||
key: "updatedAt",
|
||||
width: 150,
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: "最近执行ID",
|
||||
dataIndex: "lastExecutionId",
|
||||
key: "lastExecutionId",
|
||||
width: 150,
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: "描述",
|
||||
dataIndex: "description",
|
||||
key: "description",
|
||||
ellipsis: true,
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
key: "action",
|
||||
fixed: "right" as const,
|
||||
render: (_: any, record: CollectionTask) => {
|
||||
return taskOperations(record).map((op) => {
|
||||
const button = (
|
||||
<Tooltip key={op.key} title={op.label}>
|
||||
<Button
|
||||
type="text"
|
||||
icon={op.icon}
|
||||
danger={op?.danger}
|
||||
onClick={() => op.onClick(record)}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
if (op.confirm) {
|
||||
return (
|
||||
<Popconfirm
|
||||
key={op.key}
|
||||
title={op.confirm.title}
|
||||
okText={op.confirm.okText}
|
||||
cancelText={op.confirm.cancelText}
|
||||
okType={op.danger ? "danger" : "primary"}
|
||||
onConfirm={() => op.onClick(record)}
|
||||
>
|
||||
<Tooltip key={op.key} title={op.label}>
|
||||
<Button type="text" icon={op.icon} danger={op?.danger} />
|
||||
</Tooltip>
|
||||
</Popconfirm>
|
||||
);
|
||||
}
|
||||
return button;
|
||||
});
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Header Actions */}
|
||||
<SearchControls
|
||||
searchTerm={searchParams.keyword}
|
||||
onSearchChange={(newSearchTerm) =>
|
||||
setSearchParams((prev) => ({
|
||||
...prev,
|
||||
keyword: newSearchTerm,
|
||||
current: 1,
|
||||
}))
|
||||
}
|
||||
searchPlaceholder="搜索任务名称或描述..."
|
||||
filters={filters}
|
||||
onFiltersChange={handleFiltersChange}
|
||||
showViewToggle={false}
|
||||
onClearFilters={() =>
|
||||
setSearchParams((prev) => ({
|
||||
...prev,
|
||||
filters: {},
|
||||
}))
|
||||
}
|
||||
onReload={fetchData}
|
||||
/>
|
||||
|
||||
{/* Tasks Table */}
|
||||
<Card>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={tableData}
|
||||
loading={loading}
|
||||
rowKey="id"
|
||||
pagination={{
|
||||
...pagination,
|
||||
current: searchParams.current,
|
||||
pageSize: searchParams.pageSize,
|
||||
total: pagination.total,
|
||||
}}
|
||||
scroll={{ x: "max-content", y: "calc(100vh - 25rem)" }}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
import {
|
||||
Card,
|
||||
Button,
|
||||
Badge,
|
||||
Table,
|
||||
Dropdown,
|
||||
App,
|
||||
Tooltip,
|
||||
Popconfirm,
|
||||
} from "antd";
|
||||
import {
|
||||
DeleteOutlined,
|
||||
EditOutlined,
|
||||
EllipsisOutlined,
|
||||
PauseCircleOutlined,
|
||||
PauseOutlined,
|
||||
PlayCircleOutlined,
|
||||
StopOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { SearchControls } from "@/components/SearchControls";
|
||||
import {
|
||||
deleteTaskByIdUsingDelete,
|
||||
executeTaskByIdUsingPost,
|
||||
queryTasksUsingGet,
|
||||
stopTaskByIdUsingPost,
|
||||
} from "../collection.apis";
|
||||
import { TaskStatus, type CollectionTask } from "../collection.model";
|
||||
import { StatusMap, SyncModeMap } from "../collection.const";
|
||||
import useFetchData from "@/hooks/useFetchData";
|
||||
import { useNavigate } from "react-router";
|
||||
import { mapCollectionTask } from "../collection.const";
|
||||
|
||||
export default function TaskManagement() {
|
||||
const { message } = App.useApp();
|
||||
const navigate = useNavigate();
|
||||
const filters = [
|
||||
{
|
||||
key: "status",
|
||||
label: "状态筛选",
|
||||
options: [
|
||||
{ value: "all", label: "全部状态" },
|
||||
...Object.values(StatusMap),
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const {
|
||||
loading,
|
||||
tableData,
|
||||
pagination,
|
||||
searchParams,
|
||||
setSearchParams,
|
||||
fetchData,
|
||||
handleFiltersChange,
|
||||
} = useFetchData(queryTasksUsingGet, mapCollectionTask);
|
||||
|
||||
const handleStartTask = async (taskId: string) => {
|
||||
await executeTaskByIdUsingPost(taskId);
|
||||
message.success("任务启动请求已发送");
|
||||
fetchData();
|
||||
};
|
||||
|
||||
const handleStopTask = async (taskId: string) => {
|
||||
await stopTaskByIdUsingPost(taskId);
|
||||
message.success("任务停止请求已发送");
|
||||
fetchData();
|
||||
};
|
||||
|
||||
const handleDeleteTask = async (taskId: string) => {
|
||||
await deleteTaskByIdUsingDelete(taskId);
|
||||
message.success("任务已删除");
|
||||
fetchData();
|
||||
};
|
||||
|
||||
const taskOperations = (record: CollectionTask) => {
|
||||
const isStopped = record.status === TaskStatus.STOPPED;
|
||||
const startButton = {
|
||||
key: "start",
|
||||
label: "启动",
|
||||
icon: <PlayCircleOutlined />,
|
||||
onClick: () => handleStartTask(record.id),
|
||||
};
|
||||
const stopButton = {
|
||||
key: "stop",
|
||||
label: "停止",
|
||||
icon: <PauseCircleOutlined />,
|
||||
onClick: () => handleStopTask(record.id),
|
||||
};
|
||||
const items = [
|
||||
// isStopped ? startButton : stopButton,
|
||||
// {
|
||||
// key: "edit",
|
||||
// label: "编辑",
|
||||
// icon: <EditOutlined />,
|
||||
// onClick: () => {
|
||||
// showEditTaskModal(record);
|
||||
// },
|
||||
// },
|
||||
{
|
||||
key: "delete",
|
||||
label: "删除",
|
||||
danger: true,
|
||||
icon: <DeleteOutlined />,
|
||||
confirm: {
|
||||
title: "确定要删除该任务吗?此操作不可撤销。",
|
||||
okText: "删除",
|
||||
cancelText: "取消",
|
||||
okType: "danger",
|
||||
},
|
||||
onClick: () => handleDeleteTask(record.id),
|
||||
},
|
||||
];
|
||||
return items;
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: "任务名称",
|
||||
dataIndex: "name",
|
||||
key: "name",
|
||||
fixed: "left",
|
||||
width: 150,
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: "状态",
|
||||
dataIndex: "status",
|
||||
key: "status",
|
||||
width: 150,
|
||||
ellipsis: true,
|
||||
render: (status: string) => (
|
||||
<Badge text={status.label} color={status.color} />
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "同步方式",
|
||||
dataIndex: "syncMode",
|
||||
key: "syncMode",
|
||||
width: 150,
|
||||
ellipsis: true,
|
||||
render: (text: string) => <span>{SyncModeMap[text]?.label}</span>,
|
||||
},
|
||||
{
|
||||
title: "创建时间",
|
||||
dataIndex: "createdAt",
|
||||
key: "createdAt",
|
||||
width: 150,
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: "更新时间",
|
||||
dataIndex: "updatedAt",
|
||||
key: "updatedAt",
|
||||
width: 150,
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: "最近执行ID",
|
||||
dataIndex: "lastExecutionId",
|
||||
key: "lastExecutionId",
|
||||
width: 150,
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: "描述",
|
||||
dataIndex: "description",
|
||||
key: "description",
|
||||
ellipsis: true,
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
key: "action",
|
||||
fixed: "right" as const,
|
||||
render: (_: any, record: CollectionTask) => {
|
||||
return taskOperations(record).map((op) => {
|
||||
const button = (
|
||||
<Tooltip key={op.key} title={op.label}>
|
||||
<Button
|
||||
type="text"
|
||||
icon={op.icon}
|
||||
danger={op?.danger}
|
||||
onClick={() => op.onClick(record)}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
if (op.confirm) {
|
||||
return (
|
||||
<Popconfirm
|
||||
key={op.key}
|
||||
title={op.confirm.title}
|
||||
okText={op.confirm.okText}
|
||||
cancelText={op.confirm.cancelText}
|
||||
okType={op.danger ? "danger" : "primary"}
|
||||
onConfirm={() => op.onClick(record)}
|
||||
>
|
||||
<Tooltip key={op.key} title={op.label}>
|
||||
<Button type="text" icon={op.icon} danger={op?.danger} />
|
||||
</Tooltip>
|
||||
</Popconfirm>
|
||||
);
|
||||
}
|
||||
return button;
|
||||
});
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Header Actions */}
|
||||
<SearchControls
|
||||
searchTerm={searchParams.keyword}
|
||||
onSearchChange={(newSearchTerm) =>
|
||||
setSearchParams((prev) => ({
|
||||
...prev,
|
||||
keyword: newSearchTerm,
|
||||
current: 1,
|
||||
}))
|
||||
}
|
||||
searchPlaceholder="搜索任务名称或描述..."
|
||||
filters={filters}
|
||||
onFiltersChange={handleFiltersChange}
|
||||
showViewToggle={false}
|
||||
onClearFilters={() =>
|
||||
setSearchParams((prev) => ({
|
||||
...prev,
|
||||
filters: {},
|
||||
}))
|
||||
}
|
||||
onReload={fetchData}
|
||||
/>
|
||||
|
||||
{/* Tasks Table */}
|
||||
<Card>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={tableData}
|
||||
loading={loading}
|
||||
rowKey="id"
|
||||
pagination={{
|
||||
...pagination,
|
||||
current: searchParams.current,
|
||||
pageSize: searchParams.pageSize,
|
||||
total: pagination.total,
|
||||
}}
|
||||
scroll={{ x: "max-content", y: "calc(100vh - 25rem)" }}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,60 +1,60 @@
|
||||
import { get, post, put, del } from "@/utils/request";
|
||||
|
||||
// 数据源任务相关接口
|
||||
export function queryTasksUsingGet(params?: any) {
|
||||
return get("/api/data-collection/tasks", params);
|
||||
}
|
||||
|
||||
export function createTaskUsingPost(data: any) {
|
||||
return post("/api/data-collection/tasks", data);
|
||||
}
|
||||
|
||||
export function queryTaskByIdUsingGet(id: string | number) {
|
||||
return get(`/api/data-collection/tasks/${id}`);
|
||||
}
|
||||
|
||||
export function updateTaskByIdUsingPut(
|
||||
id: string | number,
|
||||
data: any
|
||||
) {
|
||||
return put(`/api/data-collection/tasks/${id}`, data);
|
||||
}
|
||||
|
||||
export function queryTaskDetailsByIdUsingGet(id: string | number) {
|
||||
return get(`/api/data-collection/tasks/${id}`);
|
||||
}
|
||||
|
||||
export function queryDataXTemplatesUsingGet(params?: any) {
|
||||
return get("/api/data-collection/templates", params);
|
||||
}
|
||||
export function deleteTaskByIdUsingDelete(id: string | number) {
|
||||
return del(`/api/data-collection/tasks/${id}`);
|
||||
}
|
||||
|
||||
export function executeTaskByIdUsingPost(
|
||||
id: string | number,
|
||||
data?: any
|
||||
) {
|
||||
return post(`/api/data-collection/tasks/${id}/execute`, data);
|
||||
}
|
||||
|
||||
export function stopTaskByIdUsingPost(
|
||||
id: string | number,
|
||||
data?: any
|
||||
) {
|
||||
return post(`/api/data-collection/tasks/${id}/stop`, data);
|
||||
}
|
||||
|
||||
// 执行日志相关接口
|
||||
export function queryExecutionLogUsingPost(params?: any) {
|
||||
return post("/api/data-collection/executions", params);
|
||||
}
|
||||
|
||||
export function queryExecutionLogByIdUsingGet(id: string | number) {
|
||||
return get(`/api/data-collection/executions/${id}`);
|
||||
}
|
||||
|
||||
// 监控统计相关接口
|
||||
export function queryCollectionStatisticsUsingGet(params?: any) {
|
||||
return get("/api/data-collection/monitor/statistics", params);
|
||||
}
|
||||
import { get, post, put, del } from "@/utils/request";
|
||||
|
||||
// 数据源任务相关接口
|
||||
export function queryTasksUsingGet(params?: any) {
|
||||
return get("/api/data-collection/tasks", params);
|
||||
}
|
||||
|
||||
export function createTaskUsingPost(data: any) {
|
||||
return post("/api/data-collection/tasks", data);
|
||||
}
|
||||
|
||||
export function queryTaskByIdUsingGet(id: string | number) {
|
||||
return get(`/api/data-collection/tasks/${id}`);
|
||||
}
|
||||
|
||||
export function updateTaskByIdUsingPut(
|
||||
id: string | number,
|
||||
data: any
|
||||
) {
|
||||
return put(`/api/data-collection/tasks/${id}`, data);
|
||||
}
|
||||
|
||||
export function queryTaskDetailsByIdUsingGet(id: string | number) {
|
||||
return get(`/api/data-collection/tasks/${id}`);
|
||||
}
|
||||
|
||||
export function queryDataXTemplatesUsingGet(params?: any) {
|
||||
return get("/api/data-collection/templates", params);
|
||||
}
|
||||
export function deleteTaskByIdUsingDelete(id: string | number) {
|
||||
return del(`/api/data-collection/tasks/${id}`);
|
||||
}
|
||||
|
||||
export function executeTaskByIdUsingPost(
|
||||
id: string | number,
|
||||
data?: any
|
||||
) {
|
||||
return post(`/api/data-collection/tasks/${id}/execute`, data);
|
||||
}
|
||||
|
||||
export function stopTaskByIdUsingPost(
|
||||
id: string | number,
|
||||
data?: any
|
||||
) {
|
||||
return post(`/api/data-collection/tasks/${id}/stop`, data);
|
||||
}
|
||||
|
||||
// 执行日志相关接口
|
||||
export function queryExecutionLogUsingPost(params?: any) {
|
||||
return post("/api/data-collection/executions", params);
|
||||
}
|
||||
|
||||
export function queryExecutionLogByIdUsingGet(id: string | number) {
|
||||
return get(`/api/data-collection/executions/${id}`);
|
||||
}
|
||||
|
||||
// 监控统计相关接口
|
||||
export function queryCollectionStatisticsUsingGet(params?: any) {
|
||||
return get("/api/data-collection/monitor/statistics", params);
|
||||
}
|
||||
|
||||
@@ -1,81 +1,81 @@
|
||||
import {
|
||||
LogStatus,
|
||||
SyncMode,
|
||||
TaskStatus,
|
||||
TriggerType,
|
||||
} from "./collection.model";
|
||||
|
||||
export const StatusMap: Record<
|
||||
TaskStatus,
|
||||
{ label: string; color: string; value: TaskStatus }
|
||||
> = {
|
||||
[TaskStatus.RUNNING]: {
|
||||
label: "运行",
|
||||
color: "blue",
|
||||
value: TaskStatus.RUNNING,
|
||||
},
|
||||
[TaskStatus.STOPPED]: {
|
||||
label: "停止",
|
||||
color: "gray",
|
||||
value: TaskStatus.STOPPED,
|
||||
},
|
||||
[TaskStatus.FAILED]: {
|
||||
label: "错误",
|
||||
color: "red",
|
||||
value: TaskStatus.FAILED,
|
||||
},
|
||||
[TaskStatus.SUCCESS]: {
|
||||
label: "成功",
|
||||
color: "green",
|
||||
value: TaskStatus.SUCCESS,
|
||||
},
|
||||
[TaskStatus.DRAFT]: {
|
||||
label: "草稿",
|
||||
color: "orange",
|
||||
value: TaskStatus.DRAFT,
|
||||
},
|
||||
[TaskStatus.READY]: { label: "就绪", color: "cyan", value: TaskStatus.READY },
|
||||
};
|
||||
|
||||
export const SyncModeMap: Record<SyncMode, { label: string; value: SyncMode }> =
|
||||
{
|
||||
[SyncMode.ONCE]: { label: "立即同步", value: SyncMode.ONCE },
|
||||
[SyncMode.SCHEDULED]: { label: "定时同步", value: SyncMode.SCHEDULED },
|
||||
};
|
||||
|
||||
export const LogStatusMap: Record<
|
||||
LogStatus,
|
||||
{ label: string; color: string; value: LogStatus }
|
||||
> = {
|
||||
[LogStatus.SUCCESS]: {
|
||||
label: "成功",
|
||||
color: "green",
|
||||
value: LogStatus.SUCCESS,
|
||||
},
|
||||
[LogStatus.FAILED]: {
|
||||
label: "失败",
|
||||
color: "red",
|
||||
value: LogStatus.FAILED,
|
||||
},
|
||||
[LogStatus.RUNNING]: {
|
||||
label: "运行中",
|
||||
color: "blue",
|
||||
value: LogStatus.RUNNING,
|
||||
},
|
||||
};
|
||||
|
||||
export const LogTriggerTypeMap: Record<
|
||||
TriggerType,
|
||||
{ label: string; value: TriggerType }
|
||||
> = {
|
||||
[TriggerType.MANUAL]: { label: "手动", value: TriggerType.MANUAL },
|
||||
[TriggerType.SCHEDULED]: { label: "定时", value: TriggerType.SCHEDULED },
|
||||
[TriggerType.API]: { label: "API", value: TriggerType.API },
|
||||
};
|
||||
|
||||
export function mapCollectionTask(task: CollectionTask): CollectionTask {
|
||||
return {
|
||||
...task,
|
||||
status: StatusMap[task.status],
|
||||
};
|
||||
}
|
||||
import {
|
||||
LogStatus,
|
||||
SyncMode,
|
||||
TaskStatus,
|
||||
TriggerType,
|
||||
} from "./collection.model";
|
||||
|
||||
export const StatusMap: Record<
|
||||
TaskStatus,
|
||||
{ label: string; color: string; value: TaskStatus }
|
||||
> = {
|
||||
[TaskStatus.RUNNING]: {
|
||||
label: "运行",
|
||||
color: "blue",
|
||||
value: TaskStatus.RUNNING,
|
||||
},
|
||||
[TaskStatus.STOPPED]: {
|
||||
label: "停止",
|
||||
color: "gray",
|
||||
value: TaskStatus.STOPPED,
|
||||
},
|
||||
[TaskStatus.FAILED]: {
|
||||
label: "错误",
|
||||
color: "red",
|
||||
value: TaskStatus.FAILED,
|
||||
},
|
||||
[TaskStatus.SUCCESS]: {
|
||||
label: "成功",
|
||||
color: "green",
|
||||
value: TaskStatus.SUCCESS,
|
||||
},
|
||||
[TaskStatus.DRAFT]: {
|
||||
label: "草稿",
|
||||
color: "orange",
|
||||
value: TaskStatus.DRAFT,
|
||||
},
|
||||
[TaskStatus.READY]: { label: "就绪", color: "cyan", value: TaskStatus.READY },
|
||||
};
|
||||
|
||||
export const SyncModeMap: Record<SyncMode, { label: string; value: SyncMode }> =
|
||||
{
|
||||
[SyncMode.ONCE]: { label: "立即同步", value: SyncMode.ONCE },
|
||||
[SyncMode.SCHEDULED]: { label: "定时同步", value: SyncMode.SCHEDULED },
|
||||
};
|
||||
|
||||
export const LogStatusMap: Record<
|
||||
LogStatus,
|
||||
{ label: string; color: string; value: LogStatus }
|
||||
> = {
|
||||
[LogStatus.SUCCESS]: {
|
||||
label: "成功",
|
||||
color: "green",
|
||||
value: LogStatus.SUCCESS,
|
||||
},
|
||||
[LogStatus.FAILED]: {
|
||||
label: "失败",
|
||||
color: "red",
|
||||
value: LogStatus.FAILED,
|
||||
},
|
||||
[LogStatus.RUNNING]: {
|
||||
label: "运行中",
|
||||
color: "blue",
|
||||
value: LogStatus.RUNNING,
|
||||
},
|
||||
};
|
||||
|
||||
export const LogTriggerTypeMap: Record<
|
||||
TriggerType,
|
||||
{ label: string; value: TriggerType }
|
||||
> = {
|
||||
[TriggerType.MANUAL]: { label: "手动", value: TriggerType.MANUAL },
|
||||
[TriggerType.SCHEDULED]: { label: "定时", value: TriggerType.SCHEDULED },
|
||||
[TriggerType.API]: { label: "API", value: TriggerType.API },
|
||||
};
|
||||
|
||||
export function mapCollectionTask(task: CollectionTask): CollectionTask {
|
||||
return {
|
||||
...task,
|
||||
status: StatusMap[task.status],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,52 +1,52 @@
|
||||
export enum TaskStatus {
|
||||
DRAFT = "DRAFT",
|
||||
READY = "READY",
|
||||
RUNNING = "RUNNING",
|
||||
SUCCESS = "SUCCESS",
|
||||
FAILED = "FAILED",
|
||||
STOPPED = "STOPPED",
|
||||
}
|
||||
|
||||
export enum SyncMode {
|
||||
ONCE = "ONCE",
|
||||
SCHEDULED = "SCHEDULED",
|
||||
}
|
||||
|
||||
export interface CollectionTask {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
config: object; // 具体配置结构根据实际需求定义
|
||||
status: TaskStatus;
|
||||
syncMode: SyncMode;
|
||||
scheduleExpression?: string; // 仅当 syncMode 为 SCHEDULED 时存在
|
||||
lastExecutionId: string;
|
||||
createdAt: string; // ISO date string
|
||||
updatedAt: string; // ISO date string
|
||||
}
|
||||
|
||||
export enum LogStatus {
|
||||
RUNNING = "RUNNING",
|
||||
SUCCESS = "SUCCESS",
|
||||
FAILED = "FAILED",
|
||||
}
|
||||
|
||||
export enum TriggerType {
|
||||
MANUAL = "MANUAL",
|
||||
SCHEDULED = "SCHEDULED",
|
||||
API = "API",
|
||||
}
|
||||
|
||||
export interface CollectionLog {
|
||||
id: string;
|
||||
taskId: string;
|
||||
taskName: string;
|
||||
status: TaskStatus; // 任务执行状态
|
||||
triggerType: TriggerType; // 触发类型,如手动触发、定时触发等
|
||||
startTime: string; // ISO date string
|
||||
endTime: string; // ISO date string
|
||||
duration: string; // 格式化的持续时间字符串
|
||||
retryCount: number;
|
||||
processId: string;
|
||||
errorMessage?: string; // 可选,错误信息
|
||||
}
|
||||
export enum TaskStatus {
|
||||
DRAFT = "DRAFT",
|
||||
READY = "READY",
|
||||
RUNNING = "RUNNING",
|
||||
SUCCESS = "SUCCESS",
|
||||
FAILED = "FAILED",
|
||||
STOPPED = "STOPPED",
|
||||
}
|
||||
|
||||
export enum SyncMode {
|
||||
ONCE = "ONCE",
|
||||
SCHEDULED = "SCHEDULED",
|
||||
}
|
||||
|
||||
export interface CollectionTask {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
config: object; // 具体配置结构根据实际需求定义
|
||||
status: TaskStatus;
|
||||
syncMode: SyncMode;
|
||||
scheduleExpression?: string; // 仅当 syncMode 为 SCHEDULED 时存在
|
||||
lastExecutionId: string;
|
||||
createdAt: string; // ISO date string
|
||||
updatedAt: string; // ISO date string
|
||||
}
|
||||
|
||||
export enum LogStatus {
|
||||
RUNNING = "RUNNING",
|
||||
SUCCESS = "SUCCESS",
|
||||
FAILED = "FAILED",
|
||||
}
|
||||
|
||||
export enum TriggerType {
|
||||
MANUAL = "MANUAL",
|
||||
SCHEDULED = "SCHEDULED",
|
||||
API = "API",
|
||||
}
|
||||
|
||||
export interface CollectionLog {
|
||||
id: string;
|
||||
taskId: string;
|
||||
taskName: string;
|
||||
status: TaskStatus; // 任务执行状态
|
||||
triggerType: TriggerType; // 触发类型,如手动触发、定时触发等
|
||||
startTime: string; // ISO date string
|
||||
endTime: string; // ISO date string
|
||||
duration: string; // 格式化的持续时间字符串
|
||||
retryCount: number;
|
||||
processId: string;
|
||||
errorMessage?: string; // 可选,错误信息
|
||||
}
|
||||
|
||||
@@ -1,428 +1,428 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Button, Form, Input, Select, message, Modal, Row, Col, Table, Space } from 'antd';
|
||||
import { EyeOutlined } from '@ant-design/icons';
|
||||
import { queryDatasetsUsingGet } from "@/pages/DataManagement/dataset.api.ts";
|
||||
import { mapDataset } from "@/pages/DataManagement/dataset.const.tsx";
|
||||
import { queryModelListUsingGet } from "@/pages/SettingsPage/settings.apis.ts";
|
||||
import { ModelI } from "@/pages/SettingsPage/ModelAccess.tsx";
|
||||
import { createEvaluationTaskUsingPost } from "@/pages/DataEvaluation/evaluation.api.ts";
|
||||
import { queryPromptTemplatesUsingGet } from "@/pages/DataEvaluation/evaluation.api.ts";
|
||||
import PreviewPromptModal from "@/pages/DataEvaluation/Create/PreviewPrompt.tsx";
|
||||
import { EVAL_METHODS, TASK_TYPES } from "@/pages/DataEvaluation/evaluation.const.tsx";
|
||||
|
||||
interface Dataset {
|
||||
id: string;
|
||||
name: string;
|
||||
fileCount: number;
|
||||
size: string;
|
||||
}
|
||||
|
||||
interface Dimension {
|
||||
key: string;
|
||||
dimension: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
interface PromptTemplate {
|
||||
evalType: string;
|
||||
prompt: string;
|
||||
defaultDimensions: Dimension[];
|
||||
}
|
||||
|
||||
interface CreateTaskModalProps {
|
||||
visible: boolean;
|
||||
onCancel: () => void;
|
||||
onSuccess: () => void;
|
||||
}
|
||||
|
||||
const DEFAULT_EVAL_METHOD = 'AUTO';
|
||||
const DEFAULT_TASK_TYPE = 'QA';
|
||||
|
||||
const CreateTaskModal: React.FC<CreateTaskModalProps> = ({ visible, onCancel, onSuccess }) => {
|
||||
const [form] = Form.useForm();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [datasets, setDatasets] = useState<Dataset[]>([]);
|
||||
const [models, setModels] = useState<ModelI[]>([]);
|
||||
const [dimensions, setDimensions] = useState<Dimension[]>([]);
|
||||
const [newDimension, setNewDimension] = useState<Omit<Dimension, 'key'>>({
|
||||
dimension: '',
|
||||
description: ''
|
||||
});
|
||||
const [taskType, setTaskType] = useState<string>(DEFAULT_TASK_TYPE);
|
||||
const [promptTemplates, setPromptTemplates] = useState<PromptTemplate[]>([]);
|
||||
const [previewVisible, setPreviewVisible] = useState(false);
|
||||
const [evaluationPrompt, setEvaluationPrompt] = useState('');
|
||||
|
||||
const handleAddDimension = () => {
|
||||
if (!newDimension.dimension.trim()) {
|
||||
message.warning('请输入维度名称');
|
||||
return;
|
||||
}
|
||||
setDimensions([...dimensions, { ...newDimension, key: `dim-${Date.now()}` }]);
|
||||
setNewDimension({ dimension: '', description: '' });
|
||||
};
|
||||
|
||||
const handleDeleteDimension = (key: string) => {
|
||||
if (dimensions.length <= 1) {
|
||||
message.warning('至少需要保留一个评估维度');
|
||||
return;
|
||||
}
|
||||
setDimensions(dimensions.filter(item => item.key !== key));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
fetchDatasets().then();
|
||||
fetchModels().then();
|
||||
fetchPromptTemplates().then();
|
||||
// sync form with local taskType default
|
||||
form.setFieldsValue({ taskType: DEFAULT_TASK_TYPE });
|
||||
}
|
||||
}, [visible]);
|
||||
|
||||
// when promptTemplates or taskType change, switch dimensions to template defaults (COT/QA)
|
||||
useEffect(() => {
|
||||
if (!promptTemplates || promptTemplates.length === 0) return;
|
||||
const template = promptTemplates.find(t => t.evalType === taskType);
|
||||
if (template && template.defaultDimensions) {
|
||||
setDimensions(template.defaultDimensions.map((dim: any, index: number) => ({
|
||||
key: `dim-${index}`,
|
||||
dimension: dim.dimension,
|
||||
description: dim.description
|
||||
})));
|
||||
}
|
||||
}, [taskType, promptTemplates]);
|
||||
|
||||
const fetchDatasets = async () => {
|
||||
try {
|
||||
const { data } = await queryDatasetsUsingGet({ page: 1, size: 1000 });
|
||||
setDatasets(data.content.map(mapDataset) || []);
|
||||
} catch (error) {
|
||||
console.error('Error fetching datasets:', error);
|
||||
message.error('获取数据集列表失败');
|
||||
}
|
||||
};
|
||||
|
||||
const fetchModels = async () => {
|
||||
try {
|
||||
const { data } = await queryModelListUsingGet({ page: 0, size: 1000 });
|
||||
setModels(data.content || []);
|
||||
} catch (error) {
|
||||
console.error('Error fetching models:', error);
|
||||
message.error('获取模型列表失败');
|
||||
}
|
||||
};
|
||||
|
||||
const formatDimensionsForPrompt = (dimensions: Dimension[]) => {
|
||||
let result = "";
|
||||
dimensions.forEach((dim, index) => {
|
||||
if (index > 0) {
|
||||
result += "\n";
|
||||
}
|
||||
result += `### ${index + 1}. ${dim.dimension}\n**评估标准:**\n${dim.description}`;
|
||||
if (index < dimensions.length - 1) {
|
||||
result += "\n";
|
||||
}
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
const formatResultExample = (dimensions: Dimension[]) => {
|
||||
let result = "";
|
||||
dimensions.forEach((dim, index) => {
|
||||
if (index > 0) {
|
||||
result += "\n ";
|
||||
}
|
||||
result += `"${dim.dimension}": "Y"`;
|
||||
if (index < dimensions.length - 1) {
|
||||
result += ",";
|
||||
}
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
const fetchPromptTemplates = async () => {
|
||||
try {
|
||||
const response = await queryPromptTemplatesUsingGet();
|
||||
const templates: PromptTemplate[] = response.data?.templates || [];
|
||||
setPromptTemplates(templates);
|
||||
// if a template exists for current taskType, initialize dimensions (handled also by useEffect)
|
||||
const template = templates.find(t => t.evalType === taskType);
|
||||
if (template) {
|
||||
setDimensions(template.defaultDimensions.map((dim: any, index: number) => ({
|
||||
key: `dim-${index}`,
|
||||
dimension: dim.dimension,
|
||||
description: dim.description
|
||||
})));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching prompt templates:', error);
|
||||
message.error('获取评估维度失败');
|
||||
}
|
||||
};
|
||||
|
||||
const generateEvaluationPrompt = () => {
|
||||
if (dimensions.length === 0) {
|
||||
message.warning('请先添加评估维度');
|
||||
return;
|
||||
}
|
||||
const template = promptTemplates.find(t => t.evalType === taskType);
|
||||
const basePrompt = template?.prompt || '';
|
||||
const filled = basePrompt
|
||||
.replace('{dimensions}', formatDimensionsForPrompt(dimensions))
|
||||
.replace('{result_example}', formatResultExample(dimensions));
|
||||
setEvaluationPrompt(filled);
|
||||
setPreviewVisible(true);
|
||||
};
|
||||
|
||||
const chatModelOptions = models
|
||||
.filter((model) => model.type === "CHAT")
|
||||
.map((model) => ({
|
||||
label: `${model.modelName} (${model.provider})`,
|
||||
value: model.id,
|
||||
}));
|
||||
|
||||
const handleSubmit = async (values: any) => {
|
||||
if (dimensions.length === 0) {
|
||||
message.warning('请至少添加一个评估维度');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
const { datasetId, modelId, ...rest } = values;
|
||||
const selectedDataset = datasets.find(d => d.id === datasetId);
|
||||
const selectedModel = models.find(d => d.id === modelId);
|
||||
|
||||
const payload = {
|
||||
...rest,
|
||||
sourceType: 'DATASET',
|
||||
sourceId: datasetId,
|
||||
sourceName: selectedDataset?.name,
|
||||
evalConfig: {
|
||||
modelId: selectedModel?.id,
|
||||
dimensions: dimensions.map(d => ({
|
||||
dimension: d.dimension,
|
||||
description: d.description
|
||||
}))
|
||||
}
|
||||
};
|
||||
|
||||
await createEvaluationTaskUsingPost(payload);
|
||||
message.success('评估任务创建成功');
|
||||
onSuccess();
|
||||
form.resetFields();
|
||||
onCancel();
|
||||
} catch (error: any) {
|
||||
console.error('Error creating task:', error);
|
||||
message.error(error.response?.data?.message || '创建评估任务失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '维度',
|
||||
dataIndex: 'dimension',
|
||||
key: 'dimension',
|
||||
width: '30%',
|
||||
},
|
||||
{
|
||||
title: '描述',
|
||||
dataIndex: 'description',
|
||||
key: 'description',
|
||||
width: '60%',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: '10%',
|
||||
render: (_: any, record: any) => (
|
||||
<Space size="middle">
|
||||
<a
|
||||
onClick={() => handleDeleteDimension(record.key)}
|
||||
style={{ color: dimensions.length <= 1 ? '#ccc' : '#ff4d4f' }}
|
||||
className={dimensions.length <= 1 ? 'disabled-link' : ''}
|
||||
>
|
||||
删除
|
||||
</a>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="创建评估任务"
|
||||
open={visible}
|
||||
onCancel={onCancel}
|
||||
footer={null}
|
||||
width={800}
|
||||
destroyOnClose
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
onFinish={handleSubmit}
|
||||
initialValues={{
|
||||
evalMethod: DEFAULT_EVAL_METHOD,
|
||||
taskType: DEFAULT_TASK_TYPE,
|
||||
}}
|
||||
onValuesChange={(changed) => {
|
||||
if (changed.taskType) {
|
||||
setTaskType(changed.taskType);
|
||||
setEvaluationPrompt('');
|
||||
setPreviewVisible(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
label="任务名称"
|
||||
name="name"
|
||||
rules={[{ required: true, message: '请输入任务名称' }]}
|
||||
>
|
||||
<Input placeholder="输入任务名称" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
label="任务类型"
|
||||
name="taskType"
|
||||
rules={[{ required: true, message: '请选择任务类型' }]}
|
||||
>
|
||||
<Select options={TASK_TYPES} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Form.Item
|
||||
label="任务描述"
|
||||
name="description"
|
||||
>
|
||||
<Input.TextArea placeholder="输入任务描述(可选)" rows={3} />
|
||||
</Form.Item>
|
||||
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
label="选择数据集"
|
||||
name="datasetId"
|
||||
rules={[{ required: true, message: '请选择数据集' }]}
|
||||
>
|
||||
<Select
|
||||
placeholder="请选择要评估的数据集"
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
>
|
||||
{datasets.map((dataset) => (
|
||||
<Select.Option key={dataset.id} value={dataset.id} label={dataset.name}>
|
||||
<div className="flex justify-between w-full">
|
||||
<span>{dataset.name}</span>
|
||||
<span className="text-gray-500">{dataset.size}</span>
|
||||
</div>
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
label="评估方式"
|
||||
name="evalMethod"
|
||||
initialValue={DEFAULT_EVAL_METHOD}
|
||||
>
|
||||
<Select options={EVAL_METHODS} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Form.Item
|
||||
noStyle
|
||||
shouldUpdate={(prevValues, currentValues) =>
|
||||
prevValues.evalMethod !== currentValues.evalMethod
|
||||
}
|
||||
>
|
||||
{({ getFieldValue }) => getFieldValue('evalMethod') === 'AUTO' && (
|
||||
<>
|
||||
<Form.Item
|
||||
label="评估模型"
|
||||
name="modelId"
|
||||
rules={[{ required: true, message: '请选择评估模型' }]}
|
||||
>
|
||||
<Select
|
||||
placeholder="请选择模型"
|
||||
options={chatModelOptions}
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="评估维度">
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={dimensions}
|
||||
pagination={false}
|
||||
size="small"
|
||||
rowKey="key"
|
||||
/>
|
||||
<div style={{ display: 'flex', gap: 8, marginBottom: 16 }}>
|
||||
<Input
|
||||
placeholder="输入维度名称"
|
||||
value={newDimension.dimension}
|
||||
onChange={(e) => setNewDimension({...newDimension, dimension: e.target.value})}
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
<Input
|
||||
placeholder="输入维度描述"
|
||||
value={newDimension.description}
|
||||
onChange={(e) => setNewDimension({...newDimension, description: e.target.value})}
|
||||
style={{ flex: 2 }}
|
||||
/>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={handleAddDimension}
|
||||
disabled={!newDimension.dimension.trim()}
|
||||
>
|
||||
添加维度
|
||||
</Button>
|
||||
</div>
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginTop: '16px' }}>
|
||||
<Button
|
||||
type="link"
|
||||
icon={<EyeOutlined />}
|
||||
onClick={generateEvaluationPrompt}
|
||||
>
|
||||
查看评估提示词
|
||||
</Button>
|
||||
</div>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
<div style={{ textAlign: 'right' }}>
|
||||
<Button onClick={onCancel} style={{ marginRight: 8 }}>
|
||||
取消
|
||||
</Button>
|
||||
<Button type="primary" htmlType="submit" loading={loading}>
|
||||
创建任务
|
||||
</Button>
|
||||
</div>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
<PreviewPromptModal
|
||||
previewVisible={previewVisible}
|
||||
onCancel={() => setPreviewVisible(false)}
|
||||
evaluationPrompt={evaluationPrompt}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateTaskModal;
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Button, Form, Input, Select, message, Modal, Row, Col, Table, Space } from 'antd';
|
||||
import { EyeOutlined } from '@ant-design/icons';
|
||||
import { queryDatasetsUsingGet } from "@/pages/DataManagement/dataset.api.ts";
|
||||
import { mapDataset } from "@/pages/DataManagement/dataset.const.tsx";
|
||||
import { queryModelListUsingGet } from "@/pages/SettingsPage/settings.apis.ts";
|
||||
import { ModelI } from "@/pages/SettingsPage/ModelAccess.tsx";
|
||||
import { createEvaluationTaskUsingPost } from "@/pages/DataEvaluation/evaluation.api.ts";
|
||||
import { queryPromptTemplatesUsingGet } from "@/pages/DataEvaluation/evaluation.api.ts";
|
||||
import PreviewPromptModal from "@/pages/DataEvaluation/Create/PreviewPrompt.tsx";
|
||||
import { EVAL_METHODS, TASK_TYPES } from "@/pages/DataEvaluation/evaluation.const.tsx";
|
||||
|
||||
interface Dataset {
|
||||
id: string;
|
||||
name: string;
|
||||
fileCount: number;
|
||||
size: string;
|
||||
}
|
||||
|
||||
interface Dimension {
|
||||
key: string;
|
||||
dimension: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
interface PromptTemplate {
|
||||
evalType: string;
|
||||
prompt: string;
|
||||
defaultDimensions: Dimension[];
|
||||
}
|
||||
|
||||
interface CreateTaskModalProps {
|
||||
visible: boolean;
|
||||
onCancel: () => void;
|
||||
onSuccess: () => void;
|
||||
}
|
||||
|
||||
const DEFAULT_EVAL_METHOD = 'AUTO';
|
||||
const DEFAULT_TASK_TYPE = 'QA';
|
||||
|
||||
const CreateTaskModal: React.FC<CreateTaskModalProps> = ({ visible, onCancel, onSuccess }) => {
|
||||
const [form] = Form.useForm();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [datasets, setDatasets] = useState<Dataset[]>([]);
|
||||
const [models, setModels] = useState<ModelI[]>([]);
|
||||
const [dimensions, setDimensions] = useState<Dimension[]>([]);
|
||||
const [newDimension, setNewDimension] = useState<Omit<Dimension, 'key'>>({
|
||||
dimension: '',
|
||||
description: ''
|
||||
});
|
||||
const [taskType, setTaskType] = useState<string>(DEFAULT_TASK_TYPE);
|
||||
const [promptTemplates, setPromptTemplates] = useState<PromptTemplate[]>([]);
|
||||
const [previewVisible, setPreviewVisible] = useState(false);
|
||||
const [evaluationPrompt, setEvaluationPrompt] = useState('');
|
||||
|
||||
const handleAddDimension = () => {
|
||||
if (!newDimension.dimension.trim()) {
|
||||
message.warning('请输入维度名称');
|
||||
return;
|
||||
}
|
||||
setDimensions([...dimensions, { ...newDimension, key: `dim-${Date.now()}` }]);
|
||||
setNewDimension({ dimension: '', description: '' });
|
||||
};
|
||||
|
||||
const handleDeleteDimension = (key: string) => {
|
||||
if (dimensions.length <= 1) {
|
||||
message.warning('至少需要保留一个评估维度');
|
||||
return;
|
||||
}
|
||||
setDimensions(dimensions.filter(item => item.key !== key));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
fetchDatasets().then();
|
||||
fetchModels().then();
|
||||
fetchPromptTemplates().then();
|
||||
// sync form with local taskType default
|
||||
form.setFieldsValue({ taskType: DEFAULT_TASK_TYPE });
|
||||
}
|
||||
}, [visible]);
|
||||
|
||||
// when promptTemplates or taskType change, switch dimensions to template defaults (COT/QA)
|
||||
useEffect(() => {
|
||||
if (!promptTemplates || promptTemplates.length === 0) return;
|
||||
const template = promptTemplates.find(t => t.evalType === taskType);
|
||||
if (template && template.defaultDimensions) {
|
||||
setDimensions(template.defaultDimensions.map((dim: any, index: number) => ({
|
||||
key: `dim-${index}`,
|
||||
dimension: dim.dimension,
|
||||
description: dim.description
|
||||
})));
|
||||
}
|
||||
}, [taskType, promptTemplates]);
|
||||
|
||||
const fetchDatasets = async () => {
|
||||
try {
|
||||
const { data } = await queryDatasetsUsingGet({ page: 1, size: 1000 });
|
||||
setDatasets(data.content.map(mapDataset) || []);
|
||||
} catch (error) {
|
||||
console.error('Error fetching datasets:', error);
|
||||
message.error('获取数据集列表失败');
|
||||
}
|
||||
};
|
||||
|
||||
const fetchModels = async () => {
|
||||
try {
|
||||
const { data } = await queryModelListUsingGet({ page: 0, size: 1000 });
|
||||
setModels(data.content || []);
|
||||
} catch (error) {
|
||||
console.error('Error fetching models:', error);
|
||||
message.error('获取模型列表失败');
|
||||
}
|
||||
};
|
||||
|
||||
const formatDimensionsForPrompt = (dimensions: Dimension[]) => {
|
||||
let result = "";
|
||||
dimensions.forEach((dim, index) => {
|
||||
if (index > 0) {
|
||||
result += "\n";
|
||||
}
|
||||
result += `### ${index + 1}. ${dim.dimension}\n**评估标准:**\n${dim.description}`;
|
||||
if (index < dimensions.length - 1) {
|
||||
result += "\n";
|
||||
}
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
const formatResultExample = (dimensions: Dimension[]) => {
|
||||
let result = "";
|
||||
dimensions.forEach((dim, index) => {
|
||||
if (index > 0) {
|
||||
result += "\n ";
|
||||
}
|
||||
result += `"${dim.dimension}": "Y"`;
|
||||
if (index < dimensions.length - 1) {
|
||||
result += ",";
|
||||
}
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
const fetchPromptTemplates = async () => {
|
||||
try {
|
||||
const response = await queryPromptTemplatesUsingGet();
|
||||
const templates: PromptTemplate[] = response.data?.templates || [];
|
||||
setPromptTemplates(templates);
|
||||
// if a template exists for current taskType, initialize dimensions (handled also by useEffect)
|
||||
const template = templates.find(t => t.evalType === taskType);
|
||||
if (template) {
|
||||
setDimensions(template.defaultDimensions.map((dim: any, index: number) => ({
|
||||
key: `dim-${index}`,
|
||||
dimension: dim.dimension,
|
||||
description: dim.description
|
||||
})));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching prompt templates:', error);
|
||||
message.error('获取评估维度失败');
|
||||
}
|
||||
};
|
||||
|
||||
const generateEvaluationPrompt = () => {
|
||||
if (dimensions.length === 0) {
|
||||
message.warning('请先添加评估维度');
|
||||
return;
|
||||
}
|
||||
const template = promptTemplates.find(t => t.evalType === taskType);
|
||||
const basePrompt = template?.prompt || '';
|
||||
const filled = basePrompt
|
||||
.replace('{dimensions}', formatDimensionsForPrompt(dimensions))
|
||||
.replace('{result_example}', formatResultExample(dimensions));
|
||||
setEvaluationPrompt(filled);
|
||||
setPreviewVisible(true);
|
||||
};
|
||||
|
||||
const chatModelOptions = models
|
||||
.filter((model) => model.type === "CHAT")
|
||||
.map((model) => ({
|
||||
label: `${model.modelName} (${model.provider})`,
|
||||
value: model.id,
|
||||
}));
|
||||
|
||||
const handleSubmit = async (values: any) => {
|
||||
if (dimensions.length === 0) {
|
||||
message.warning('请至少添加一个评估维度');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
const { datasetId, modelId, ...rest } = values;
|
||||
const selectedDataset = datasets.find(d => d.id === datasetId);
|
||||
const selectedModel = models.find(d => d.id === modelId);
|
||||
|
||||
const payload = {
|
||||
...rest,
|
||||
sourceType: 'DATASET',
|
||||
sourceId: datasetId,
|
||||
sourceName: selectedDataset?.name,
|
||||
evalConfig: {
|
||||
modelId: selectedModel?.id,
|
||||
dimensions: dimensions.map(d => ({
|
||||
dimension: d.dimension,
|
||||
description: d.description
|
||||
}))
|
||||
}
|
||||
};
|
||||
|
||||
await createEvaluationTaskUsingPost(payload);
|
||||
message.success('评估任务创建成功');
|
||||
onSuccess();
|
||||
form.resetFields();
|
||||
onCancel();
|
||||
} catch (error: any) {
|
||||
console.error('Error creating task:', error);
|
||||
message.error(error.response?.data?.message || '创建评估任务失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '维度',
|
||||
dataIndex: 'dimension',
|
||||
key: 'dimension',
|
||||
width: '30%',
|
||||
},
|
||||
{
|
||||
title: '描述',
|
||||
dataIndex: 'description',
|
||||
key: 'description',
|
||||
width: '60%',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: '10%',
|
||||
render: (_: any, record: any) => (
|
||||
<Space size="middle">
|
||||
<a
|
||||
onClick={() => handleDeleteDimension(record.key)}
|
||||
style={{ color: dimensions.length <= 1 ? '#ccc' : '#ff4d4f' }}
|
||||
className={dimensions.length <= 1 ? 'disabled-link' : ''}
|
||||
>
|
||||
删除
|
||||
</a>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="创建评估任务"
|
||||
open={visible}
|
||||
onCancel={onCancel}
|
||||
footer={null}
|
||||
width={800}
|
||||
destroyOnClose
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
onFinish={handleSubmit}
|
||||
initialValues={{
|
||||
evalMethod: DEFAULT_EVAL_METHOD,
|
||||
taskType: DEFAULT_TASK_TYPE,
|
||||
}}
|
||||
onValuesChange={(changed) => {
|
||||
if (changed.taskType) {
|
||||
setTaskType(changed.taskType);
|
||||
setEvaluationPrompt('');
|
||||
setPreviewVisible(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
label="任务名称"
|
||||
name="name"
|
||||
rules={[{ required: true, message: '请输入任务名称' }]}
|
||||
>
|
||||
<Input placeholder="输入任务名称" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
label="任务类型"
|
||||
name="taskType"
|
||||
rules={[{ required: true, message: '请选择任务类型' }]}
|
||||
>
|
||||
<Select options={TASK_TYPES} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Form.Item
|
||||
label="任务描述"
|
||||
name="description"
|
||||
>
|
||||
<Input.TextArea placeholder="输入任务描述(可选)" rows={3} />
|
||||
</Form.Item>
|
||||
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
label="选择数据集"
|
||||
name="datasetId"
|
||||
rules={[{ required: true, message: '请选择数据集' }]}
|
||||
>
|
||||
<Select
|
||||
placeholder="请选择要评估的数据集"
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
>
|
||||
{datasets.map((dataset) => (
|
||||
<Select.Option key={dataset.id} value={dataset.id} label={dataset.name}>
|
||||
<div className="flex justify-between w-full">
|
||||
<span>{dataset.name}</span>
|
||||
<span className="text-gray-500">{dataset.size}</span>
|
||||
</div>
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
label="评估方式"
|
||||
name="evalMethod"
|
||||
initialValue={DEFAULT_EVAL_METHOD}
|
||||
>
|
||||
<Select options={EVAL_METHODS} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Form.Item
|
||||
noStyle
|
||||
shouldUpdate={(prevValues, currentValues) =>
|
||||
prevValues.evalMethod !== currentValues.evalMethod
|
||||
}
|
||||
>
|
||||
{({ getFieldValue }) => getFieldValue('evalMethod') === 'AUTO' && (
|
||||
<>
|
||||
<Form.Item
|
||||
label="评估模型"
|
||||
name="modelId"
|
||||
rules={[{ required: true, message: '请选择评估模型' }]}
|
||||
>
|
||||
<Select
|
||||
placeholder="请选择模型"
|
||||
options={chatModelOptions}
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="评估维度">
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={dimensions}
|
||||
pagination={false}
|
||||
size="small"
|
||||
rowKey="key"
|
||||
/>
|
||||
<div style={{ display: 'flex', gap: 8, marginBottom: 16 }}>
|
||||
<Input
|
||||
placeholder="输入维度名称"
|
||||
value={newDimension.dimension}
|
||||
onChange={(e) => setNewDimension({...newDimension, dimension: e.target.value})}
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
<Input
|
||||
placeholder="输入维度描述"
|
||||
value={newDimension.description}
|
||||
onChange={(e) => setNewDimension({...newDimension, description: e.target.value})}
|
||||
style={{ flex: 2 }}
|
||||
/>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={handleAddDimension}
|
||||
disabled={!newDimension.dimension.trim()}
|
||||
>
|
||||
添加维度
|
||||
</Button>
|
||||
</div>
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginTop: '16px' }}>
|
||||
<Button
|
||||
type="link"
|
||||
icon={<EyeOutlined />}
|
||||
onClick={generateEvaluationPrompt}
|
||||
>
|
||||
查看评估提示词
|
||||
</Button>
|
||||
</div>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
<div style={{ textAlign: 'right' }}>
|
||||
<Button onClick={onCancel} style={{ marginRight: 8 }}>
|
||||
取消
|
||||
</Button>
|
||||
<Button type="primary" htmlType="submit" loading={loading}>
|
||||
创建任务
|
||||
</Button>
|
||||
</div>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
<PreviewPromptModal
|
||||
previewVisible={previewVisible}
|
||||
onCancel={() => setPreviewVisible(false)}
|
||||
evaluationPrompt={evaluationPrompt}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateTaskModal;
|
||||
|
||||
@@ -1,42 +1,42 @@
|
||||
import React from 'react';
|
||||
import { Button, message, Modal } from 'antd';
|
||||
|
||||
interface PreviewPromptModalProps {
|
||||
previewVisible: boolean;
|
||||
onCancel: () => void;
|
||||
evaluationPrompt: string;
|
||||
}
|
||||
|
||||
const PreviewPromptModal: React.FC<PreviewPromptModalProps> = ({ previewVisible, onCancel, evaluationPrompt }) => {
|
||||
return (
|
||||
<Modal
|
||||
title="评估提示词预览"
|
||||
open={previewVisible}
|
||||
onCancel={onCancel}
|
||||
footer={[
|
||||
<Button key="copy" onClick={() => {
|
||||
navigator.clipboard.writeText(evaluationPrompt).then();
|
||||
message.success('已复制到剪贴板');
|
||||
}}>
|
||||
复制
|
||||
</Button>,
|
||||
<Button key="close" type="primary" onClick={onCancel}>
|
||||
关闭
|
||||
</Button>
|
||||
]}
|
||||
width={800}
|
||||
>
|
||||
<div style={{
|
||||
background: '#f5f5f5',
|
||||
padding: '16px',
|
||||
borderRadius: '4px',
|
||||
whiteSpace: 'pre-wrap',
|
||||
fontFamily: 'monospace'
|
||||
}}>
|
||||
{evaluationPrompt}
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default PreviewPromptModal;
|
||||
import React from 'react';
|
||||
import { Button, message, Modal } from 'antd';
|
||||
|
||||
interface PreviewPromptModalProps {
|
||||
previewVisible: boolean;
|
||||
onCancel: () => void;
|
||||
evaluationPrompt: string;
|
||||
}
|
||||
|
||||
const PreviewPromptModal: React.FC<PreviewPromptModalProps> = ({ previewVisible, onCancel, evaluationPrompt }) => {
|
||||
return (
|
||||
<Modal
|
||||
title="评估提示词预览"
|
||||
open={previewVisible}
|
||||
onCancel={onCancel}
|
||||
footer={[
|
||||
<Button key="copy" onClick={() => {
|
||||
navigator.clipboard.writeText(evaluationPrompt).then();
|
||||
message.success('已复制到剪贴板');
|
||||
}}>
|
||||
复制
|
||||
</Button>,
|
||||
<Button key="close" type="primary" onClick={onCancel}>
|
||||
关闭
|
||||
</Button>
|
||||
]}
|
||||
width={800}
|
||||
>
|
||||
<div style={{
|
||||
background: '#f5f5f5',
|
||||
padding: '16px',
|
||||
borderRadius: '4px',
|
||||
whiteSpace: 'pre-wrap',
|
||||
fontFamily: 'monospace'
|
||||
}}>
|
||||
{evaluationPrompt}
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default PreviewPromptModal;
|
||||
|
||||
@@ -1,152 +1,152 @@
|
||||
import { Link, useParams } from "react-router";
|
||||
import { Tabs, Spin, message, Breadcrumb } from 'antd';
|
||||
import { LayoutList, Clock } from "lucide-react";
|
||||
import { useEffect, useState } from 'react';
|
||||
import { getEvaluationTaskByIdUsingGet, queryEvaluationItemsUsingGet } from '../evaluation.api';
|
||||
import { EvaluationTask, EvaluationStatus } from '../evaluation.model';
|
||||
import DetailHeader from "@/components/DetailHeader.tsx";
|
||||
import {TaskStatusMap} from "@/pages/DataCleansing/cleansing.const.tsx";
|
||||
import EvaluationItems from "@/pages/DataEvaluation/Detail/components/EvaluationItems.tsx";
|
||||
import Overview from "@/pages/DataEvaluation/Detail/components/Overview.tsx";
|
||||
|
||||
const tabList = [
|
||||
{
|
||||
key: "overview",
|
||||
label: "概览",
|
||||
},
|
||||
{
|
||||
key: "evaluationItems",
|
||||
label: "评估详情",
|
||||
}
|
||||
];
|
||||
|
||||
interface EvaluationItem {
|
||||
id: string;
|
||||
content: string;
|
||||
status: EvaluationStatus;
|
||||
score?: number;
|
||||
dimensions: {
|
||||
id: string;
|
||||
name: string;
|
||||
score: number;
|
||||
}[];
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
const EvaluationDetailPage: React.FC = () => {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [task, setTask] = useState<EvaluationTask | null>(null);
|
||||
const [items, setItems] = useState<EvaluationItem[]>([]);
|
||||
const [activeTab, setActiveTab] = useState("overview");
|
||||
const [pagination, setPagination] = useState({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
});
|
||||
|
||||
const fetchTaskDetail = async () => {
|
||||
try {
|
||||
const response = await getEvaluationTaskByIdUsingGet(id);
|
||||
setTask(response.data);
|
||||
} catch (error) {
|
||||
message.error('Failed to fetch task details');
|
||||
console.error('Error fetching task detail:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchEvaluationItems = async (page = 1, pageSize = 10) => {
|
||||
try {
|
||||
const response = await queryEvaluationItemsUsingGet({
|
||||
taskId: id,
|
||||
page: page,
|
||||
size: pageSize,
|
||||
});
|
||||
setItems(response.data.content || []);
|
||||
setPagination({
|
||||
...pagination,
|
||||
current: page,
|
||||
total: response.data.totalElements || 0,
|
||||
});
|
||||
} catch (error) {
|
||||
message.error('Failed to fetch evaluation items');
|
||||
console.error('Error fetching evaluation items:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (id) {
|
||||
setLoading(true);
|
||||
Promise.all([
|
||||
fetchTaskDetail(),
|
||||
fetchEvaluationItems(1, pagination.pageSize),
|
||||
]).finally(() => setLoading(false));
|
||||
}
|
||||
}, [id]);
|
||||
|
||||
if (loading && !task) {
|
||||
return (
|
||||
<div style={{ textAlign: 'center', padding: '100px 0' }}>
|
||||
<Spin size="large" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!task) {
|
||||
return <div>Task not found</div>;
|
||||
}
|
||||
|
||||
const breadItems = [
|
||||
{
|
||||
title: <Link to="/data/evaluation">数据评估</Link>,
|
||||
},
|
||||
{
|
||||
title: "数据评估详情",
|
||||
},
|
||||
];
|
||||
|
||||
const headerData = {
|
||||
...task,
|
||||
icon: <LayoutList className="w-8 h-8" />,
|
||||
status: TaskStatusMap[task?.status],
|
||||
createdAt: task?.createdAt,
|
||||
lastUpdated: task?.updatedAt,
|
||||
};
|
||||
|
||||
// 基本信息描述项
|
||||
const statistics = [
|
||||
{
|
||||
icon: <Clock className="text-blue-400 w-4 h-4" />,
|
||||
key: "time",
|
||||
value: task?.updatedAt,
|
||||
},
|
||||
];
|
||||
|
||||
const operations = []
|
||||
|
||||
return (
|
||||
<>
|
||||
<Breadcrumb items={breadItems} />
|
||||
<div className="mb-4 mt-4">
|
||||
<div className="mb-4 mt-4">
|
||||
<DetailHeader
|
||||
data={headerData}
|
||||
statistics={statistics}
|
||||
operations={operations}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-overflow-auto p-6 pt-2 bg-white rounded-md shadow">
|
||||
<Tabs activeKey={activeTab} items={tabList} onChange={setActiveTab} />
|
||||
<div className="h-full overflow-auto">
|
||||
{activeTab === "overview" && <Overview task={task} />}
|
||||
{activeTab === "evaluationItems" && <EvaluationItems task={task} />}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default EvaluationDetailPage;
|
||||
import { Link, useParams } from "react-router";
|
||||
import { Tabs, Spin, message, Breadcrumb } from 'antd';
|
||||
import { LayoutList, Clock } from "lucide-react";
|
||||
import { useEffect, useState } from 'react';
|
||||
import { getEvaluationTaskByIdUsingGet, queryEvaluationItemsUsingGet } from '../evaluation.api';
|
||||
import { EvaluationTask, EvaluationStatus } from '../evaluation.model';
|
||||
import DetailHeader from "@/components/DetailHeader.tsx";
|
||||
import {TaskStatusMap} from "@/pages/DataCleansing/cleansing.const.tsx";
|
||||
import EvaluationItems from "@/pages/DataEvaluation/Detail/components/EvaluationItems.tsx";
|
||||
import Overview from "@/pages/DataEvaluation/Detail/components/Overview.tsx";
|
||||
|
||||
const tabList = [
|
||||
{
|
||||
key: "overview",
|
||||
label: "概览",
|
||||
},
|
||||
{
|
||||
key: "evaluationItems",
|
||||
label: "评估详情",
|
||||
}
|
||||
];
|
||||
|
||||
interface EvaluationItem {
|
||||
id: string;
|
||||
content: string;
|
||||
status: EvaluationStatus;
|
||||
score?: number;
|
||||
dimensions: {
|
||||
id: string;
|
||||
name: string;
|
||||
score: number;
|
||||
}[];
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
const EvaluationDetailPage: React.FC = () => {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [task, setTask] = useState<EvaluationTask | null>(null);
|
||||
const [items, setItems] = useState<EvaluationItem[]>([]);
|
||||
const [activeTab, setActiveTab] = useState("overview");
|
||||
const [pagination, setPagination] = useState({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
});
|
||||
|
||||
const fetchTaskDetail = async () => {
|
||||
try {
|
||||
const response = await getEvaluationTaskByIdUsingGet(id);
|
||||
setTask(response.data);
|
||||
} catch (error) {
|
||||
message.error('Failed to fetch task details');
|
||||
console.error('Error fetching task detail:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchEvaluationItems = async (page = 1, pageSize = 10) => {
|
||||
try {
|
||||
const response = await queryEvaluationItemsUsingGet({
|
||||
taskId: id,
|
||||
page: page,
|
||||
size: pageSize,
|
||||
});
|
||||
setItems(response.data.content || []);
|
||||
setPagination({
|
||||
...pagination,
|
||||
current: page,
|
||||
total: response.data.totalElements || 0,
|
||||
});
|
||||
} catch (error) {
|
||||
message.error('Failed to fetch evaluation items');
|
||||
console.error('Error fetching evaluation items:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (id) {
|
||||
setLoading(true);
|
||||
Promise.all([
|
||||
fetchTaskDetail(),
|
||||
fetchEvaluationItems(1, pagination.pageSize),
|
||||
]).finally(() => setLoading(false));
|
||||
}
|
||||
}, [id]);
|
||||
|
||||
if (loading && !task) {
|
||||
return (
|
||||
<div style={{ textAlign: 'center', padding: '100px 0' }}>
|
||||
<Spin size="large" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!task) {
|
||||
return <div>Task not found</div>;
|
||||
}
|
||||
|
||||
const breadItems = [
|
||||
{
|
||||
title: <Link to="/data/evaluation">数据评估</Link>,
|
||||
},
|
||||
{
|
||||
title: "数据评估详情",
|
||||
},
|
||||
];
|
||||
|
||||
const headerData = {
|
||||
...task,
|
||||
icon: <LayoutList className="w-8 h-8" />,
|
||||
status: TaskStatusMap[task?.status],
|
||||
createdAt: task?.createdAt,
|
||||
lastUpdated: task?.updatedAt,
|
||||
};
|
||||
|
||||
// 基本信息描述项
|
||||
const statistics = [
|
||||
{
|
||||
icon: <Clock className="text-blue-400 w-4 h-4" />,
|
||||
key: "time",
|
||||
value: task?.updatedAt,
|
||||
},
|
||||
];
|
||||
|
||||
const operations = []
|
||||
|
||||
return (
|
||||
<>
|
||||
<Breadcrumb items={breadItems} />
|
||||
<div className="mb-4 mt-4">
|
||||
<div className="mb-4 mt-4">
|
||||
<DetailHeader
|
||||
data={headerData}
|
||||
statistics={statistics}
|
||||
operations={operations}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-overflow-auto p-6 pt-2 bg-white rounded-md shadow">
|
||||
<Tabs activeKey={activeTab} items={tabList} onChange={setActiveTab} />
|
||||
<div className="h-full overflow-auto">
|
||||
{activeTab === "overview" && <Overview task={task} />}
|
||||
{activeTab === "evaluationItems" && <EvaluationItems task={task} />}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default EvaluationDetailPage;
|
||||
|
||||
@@ -1,257 +1,257 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Table, Typography, Button, Space, Empty, Tooltip } from 'antd';
|
||||
import { FolderOpen, FileText, ArrowLeft } from 'lucide-react';
|
||||
import { queryEvaluationFilesUsingGet, queryEvaluationItemsUsingGet } from '../../evaluation.api';
|
||||
import useFetchData from '@/hooks/useFetchData';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
const COLUMN_WIDTH = 520;
|
||||
const MONO_FONT = 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace';
|
||||
const codeBlockStyle = {
|
||||
fontFamily: MONO_FONT,
|
||||
fontSize: 12,
|
||||
lineHeight: '20px',
|
||||
color: '#334155',
|
||||
backgroundColor: '#f8fafc',
|
||||
border: '1px solid #f0f0f0',
|
||||
borderRadius: 6,
|
||||
padding: 8,
|
||||
} as const;
|
||||
|
||||
type EvalFile = {
|
||||
taskId: string;
|
||||
fileId: string;
|
||||
fileName: string;
|
||||
totalCount: number;
|
||||
evaluatedCount: number;
|
||||
pendingCount: number;
|
||||
};
|
||||
|
||||
type EvalItem = {
|
||||
id: string;
|
||||
taskId: string;
|
||||
itemId: string;
|
||||
fileId: string;
|
||||
evalContent: any;
|
||||
evalScore?: number | null;
|
||||
evalResult: any;
|
||||
status?: string;
|
||||
};
|
||||
|
||||
export default function EvaluationItems({ task }: { task: any }) {
|
||||
const [selectedFile, setSelectedFile] = useState<{ fileId: string; fileName: string } | null>(null);
|
||||
|
||||
// 文件列表数据(使用 useFetchData),pageOffset=0 表示后端分页为 1 基
|
||||
const {
|
||||
loading: loadingFiles,
|
||||
tableData: files,
|
||||
pagination: filePagination,
|
||||
setSearchParams: setFileSearchParams,
|
||||
} = useFetchData<EvalFile>(
|
||||
(params) => queryEvaluationFilesUsingGet({ taskId: task?.id, ...params }),
|
||||
(d) => d as unknown as EvalFile,
|
||||
30000,
|
||||
false,
|
||||
[],
|
||||
0
|
||||
);
|
||||
|
||||
// 评估条目数据(使用 useFetchData),依赖选中文件
|
||||
const {
|
||||
loading: loadingItems,
|
||||
tableData: items,
|
||||
pagination: itemPagination,
|
||||
setSearchParams: setItemSearchParams,
|
||||
fetchData: fetchItems,
|
||||
} = useFetchData<EvalItem>(
|
||||
(params) => {
|
||||
if (!task?.id || !selectedFile?.fileId) {
|
||||
return Promise.resolve({ data: { content: [], totalElements: 0 } });
|
||||
}
|
||||
return queryEvaluationItemsUsingGet({ taskId: task.id, file_id: selectedFile.fileId, ...params });
|
||||
},
|
||||
(d) => d as unknown as EvalItem,
|
||||
30000,
|
||||
false,
|
||||
[],
|
||||
0
|
||||
);
|
||||
|
||||
// 当选择文件变化时,主动触发一次条目查询,避免仅依赖 searchParams 变更导致未触发
|
||||
useEffect(() => {
|
||||
if (task?.id && selectedFile?.fileId) {
|
||||
setItemSearchParams((prev: any) => ({ ...prev, current: 1 }));
|
||||
// 立即拉取一次,保证点击后立刻出现数据
|
||||
fetchItems();
|
||||
}
|
||||
}, [task?.id, selectedFile?.fileId]);
|
||||
|
||||
const fileColumns = [
|
||||
{
|
||||
title: '文件名',
|
||||
dataIndex: 'fileName',
|
||||
key: 'fileName',
|
||||
render: (_: any, record: EvalFile) => (
|
||||
<Space onClick={(e) => { e.stopPropagation(); setSelectedFile({ fileId: record.fileId, fileName: record.fileName }); }} style={{ cursor: 'pointer' }}>
|
||||
<FolderOpen size={16} />
|
||||
<Button type="link" style={{ padding: 0 }}>{record.fileName}</Button>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '总条目',
|
||||
dataIndex: 'totalCount',
|
||||
key: 'totalCount',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: '已评估',
|
||||
dataIndex: 'evaluatedCount',
|
||||
key: 'evaluatedCount',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: '待评估',
|
||||
dataIndex: 'pendingCount',
|
||||
key: 'pendingCount',
|
||||
width: 120,
|
||||
},
|
||||
];
|
||||
|
||||
const renderEvalObject = (rec: EvalItem) => {
|
||||
const c = rec.evalContent;
|
||||
let jsonString = '';
|
||||
try {
|
||||
if (typeof c === 'string') {
|
||||
// 尝试将字符串解析为 JSON,失败则按原字符串显示
|
||||
try {
|
||||
jsonString = JSON.stringify(JSON.parse(c), null, 2);
|
||||
} catch {
|
||||
jsonString = JSON.stringify({ value: c }, null, 2);
|
||||
}
|
||||
} else {
|
||||
jsonString = JSON.stringify(c, null, 2);
|
||||
}
|
||||
} catch {
|
||||
jsonString = 'null';
|
||||
}
|
||||
return (
|
||||
<Tooltip
|
||||
color="#fff"
|
||||
title={<pre style={{ ...codeBlockStyle, margin: 0, maxWidth: COLUMN_WIDTH, whiteSpace: 'pre-wrap' }}>{jsonString}</pre>}
|
||||
overlayInnerStyle={{ maxHeight: 600, overflow: 'auto', width: COLUMN_WIDTH }}
|
||||
>
|
||||
<Typography.Paragraph
|
||||
style={{ margin: 0, whiteSpace: 'pre-wrap', fontFamily: MONO_FONT, fontSize: 12, lineHeight: '20px', color: '#334155' }}
|
||||
ellipsis={{ rows: 6 }}
|
||||
>
|
||||
<pre style={{ ...codeBlockStyle, whiteSpace: 'pre-wrap', margin: 0 }}>{jsonString}</pre>
|
||||
</Typography.Paragraph>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
const renderEvalResult = (rec: EvalItem) => {
|
||||
const r = rec.evalResult;
|
||||
let jsonString = '';
|
||||
try {
|
||||
if (typeof r === 'string') {
|
||||
try {
|
||||
jsonString = JSON.stringify(JSON.parse(r), null, 2);
|
||||
} catch {
|
||||
jsonString = JSON.stringify({ value: r, score: rec.evalScore ?? undefined }, null, 2);
|
||||
}
|
||||
} else {
|
||||
const withScore = rec.evalScore !== undefined && rec.evalScore !== null ? { ...r, evalScore: rec.evalScore } : r;
|
||||
jsonString = JSON.stringify(withScore, null, 2);
|
||||
}
|
||||
} catch {
|
||||
jsonString = 'null';
|
||||
}
|
||||
// 判空展示未评估
|
||||
const isEmpty = !r || (typeof r === 'string' && r.trim() === '') || (typeof r === 'object' && r !== null && Object.keys(r).length === 0);
|
||||
if (isEmpty) {
|
||||
return <Text type="secondary">未评估</Text>;
|
||||
}
|
||||
return (
|
||||
<Tooltip
|
||||
color="#fff"
|
||||
title={<pre style={{ ...codeBlockStyle, margin: 0, maxWidth: 800, whiteSpace: 'pre-wrap' }}>{jsonString}</pre>}
|
||||
overlayInnerStyle={{ maxHeight: 600, overflow: 'auto' }}
|
||||
>
|
||||
<Typography.Paragraph
|
||||
style={{ margin: 0, whiteSpace: 'pre-wrap', fontFamily: MONO_FONT, fontSize: 12, lineHeight: '20px', color: '#334155' }}
|
||||
ellipsis={{ rows: 6 }}
|
||||
>
|
||||
<pre style={{ ...codeBlockStyle, whiteSpace: 'pre-wrap', margin: 0 }}>{jsonString}</pre>
|
||||
</Typography.Paragraph>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
const itemColumns = [
|
||||
{
|
||||
title: '评估对象',
|
||||
dataIndex: 'evalContent',
|
||||
key: 'evalContent',
|
||||
render: (_: any, record: EvalItem) => renderEvalObject(record),
|
||||
width: COLUMN_WIDTH,
|
||||
},
|
||||
{
|
||||
title: '评估结果',
|
||||
dataIndex: 'evalResult',
|
||||
key: 'evalResult',
|
||||
render: (_: any, record: EvalItem) => renderEvalResult(record),
|
||||
width: COLUMN_WIDTH,
|
||||
},
|
||||
];
|
||||
|
||||
if (!task?.id) return <Empty description="任务不存在" />;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
{!selectedFile ? (
|
||||
<Table
|
||||
rowKey={(r: EvalFile) => r.fileId}
|
||||
columns={fileColumns}
|
||||
dataSource={files}
|
||||
loading={loadingFiles}
|
||||
size="middle"
|
||||
onRow={(record) => ({
|
||||
onClick: () => {
|
||||
setSelectedFile({ fileId: record.fileId, fileName: record.fileName });
|
||||
// 切换文件时,重置条目表到第一页
|
||||
setItemSearchParams((prev: any) => ({ ...prev, current: 1 }));
|
||||
},
|
||||
})}
|
||||
pagination={filePagination}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="sticky top-0 z-10 bg-white py-2" style={{ borderBottom: '1px solid #f0f0f0' }}>
|
||||
<Space wrap>
|
||||
<Button icon={<ArrowLeft size={16} />} onClick={() => { setSelectedFile(null); }}>
|
||||
返回文件列表
|
||||
</Button>
|
||||
<Space>
|
||||
<FileText size={16} />
|
||||
<Text strong>{selectedFile.fileName}</Text>
|
||||
<Text type="secondary">文件ID:{selectedFile.fileId}</Text>
|
||||
<Text type="secondary">共 {itemPagination.total} 条</Text>
|
||||
</Space>
|
||||
</Space>
|
||||
</div>
|
||||
<Table
|
||||
rowKey={(r: EvalItem) => r.id}
|
||||
columns={itemColumns}
|
||||
dataSource={items}
|
||||
loading={loadingItems}
|
||||
size="middle"
|
||||
pagination={itemPagination}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Table, Typography, Button, Space, Empty, Tooltip } from 'antd';
|
||||
import { FolderOpen, FileText, ArrowLeft } from 'lucide-react';
|
||||
import { queryEvaluationFilesUsingGet, queryEvaluationItemsUsingGet } from '../../evaluation.api';
|
||||
import useFetchData from '@/hooks/useFetchData';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
const COLUMN_WIDTH = 520;
|
||||
const MONO_FONT = 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace';
|
||||
const codeBlockStyle = {
|
||||
fontFamily: MONO_FONT,
|
||||
fontSize: 12,
|
||||
lineHeight: '20px',
|
||||
color: '#334155',
|
||||
backgroundColor: '#f8fafc',
|
||||
border: '1px solid #f0f0f0',
|
||||
borderRadius: 6,
|
||||
padding: 8,
|
||||
} as const;
|
||||
|
||||
type EvalFile = {
|
||||
taskId: string;
|
||||
fileId: string;
|
||||
fileName: string;
|
||||
totalCount: number;
|
||||
evaluatedCount: number;
|
||||
pendingCount: number;
|
||||
};
|
||||
|
||||
type EvalItem = {
|
||||
id: string;
|
||||
taskId: string;
|
||||
itemId: string;
|
||||
fileId: string;
|
||||
evalContent: any;
|
||||
evalScore?: number | null;
|
||||
evalResult: any;
|
||||
status?: string;
|
||||
};
|
||||
|
||||
export default function EvaluationItems({ task }: { task: any }) {
|
||||
const [selectedFile, setSelectedFile] = useState<{ fileId: string; fileName: string } | null>(null);
|
||||
|
||||
// 文件列表数据(使用 useFetchData),pageOffset=0 表示后端分页为 1 基
|
||||
const {
|
||||
loading: loadingFiles,
|
||||
tableData: files,
|
||||
pagination: filePagination,
|
||||
setSearchParams: setFileSearchParams,
|
||||
} = useFetchData<EvalFile>(
|
||||
(params) => queryEvaluationFilesUsingGet({ taskId: task?.id, ...params }),
|
||||
(d) => d as unknown as EvalFile,
|
||||
30000,
|
||||
false,
|
||||
[],
|
||||
0
|
||||
);
|
||||
|
||||
// 评估条目数据(使用 useFetchData),依赖选中文件
|
||||
const {
|
||||
loading: loadingItems,
|
||||
tableData: items,
|
||||
pagination: itemPagination,
|
||||
setSearchParams: setItemSearchParams,
|
||||
fetchData: fetchItems,
|
||||
} = useFetchData<EvalItem>(
|
||||
(params) => {
|
||||
if (!task?.id || !selectedFile?.fileId) {
|
||||
return Promise.resolve({ data: { content: [], totalElements: 0 } });
|
||||
}
|
||||
return queryEvaluationItemsUsingGet({ taskId: task.id, file_id: selectedFile.fileId, ...params });
|
||||
},
|
||||
(d) => d as unknown as EvalItem,
|
||||
30000,
|
||||
false,
|
||||
[],
|
||||
0
|
||||
);
|
||||
|
||||
// 当选择文件变化时,主动触发一次条目查询,避免仅依赖 searchParams 变更导致未触发
|
||||
useEffect(() => {
|
||||
if (task?.id && selectedFile?.fileId) {
|
||||
setItemSearchParams((prev: any) => ({ ...prev, current: 1 }));
|
||||
// 立即拉取一次,保证点击后立刻出现数据
|
||||
fetchItems();
|
||||
}
|
||||
}, [task?.id, selectedFile?.fileId]);
|
||||
|
||||
const fileColumns = [
|
||||
{
|
||||
title: '文件名',
|
||||
dataIndex: 'fileName',
|
||||
key: 'fileName',
|
||||
render: (_: any, record: EvalFile) => (
|
||||
<Space onClick={(e) => { e.stopPropagation(); setSelectedFile({ fileId: record.fileId, fileName: record.fileName }); }} style={{ cursor: 'pointer' }}>
|
||||
<FolderOpen size={16} />
|
||||
<Button type="link" style={{ padding: 0 }}>{record.fileName}</Button>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '总条目',
|
||||
dataIndex: 'totalCount',
|
||||
key: 'totalCount',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: '已评估',
|
||||
dataIndex: 'evaluatedCount',
|
||||
key: 'evaluatedCount',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: '待评估',
|
||||
dataIndex: 'pendingCount',
|
||||
key: 'pendingCount',
|
||||
width: 120,
|
||||
},
|
||||
];
|
||||
|
||||
const renderEvalObject = (rec: EvalItem) => {
|
||||
const c = rec.evalContent;
|
||||
let jsonString = '';
|
||||
try {
|
||||
if (typeof c === 'string') {
|
||||
// 尝试将字符串解析为 JSON,失败则按原字符串显示
|
||||
try {
|
||||
jsonString = JSON.stringify(JSON.parse(c), null, 2);
|
||||
} catch {
|
||||
jsonString = JSON.stringify({ value: c }, null, 2);
|
||||
}
|
||||
} else {
|
||||
jsonString = JSON.stringify(c, null, 2);
|
||||
}
|
||||
} catch {
|
||||
jsonString = 'null';
|
||||
}
|
||||
return (
|
||||
<Tooltip
|
||||
color="#fff"
|
||||
title={<pre style={{ ...codeBlockStyle, margin: 0, maxWidth: COLUMN_WIDTH, whiteSpace: 'pre-wrap' }}>{jsonString}</pre>}
|
||||
overlayInnerStyle={{ maxHeight: 600, overflow: 'auto', width: COLUMN_WIDTH }}
|
||||
>
|
||||
<Typography.Paragraph
|
||||
style={{ margin: 0, whiteSpace: 'pre-wrap', fontFamily: MONO_FONT, fontSize: 12, lineHeight: '20px', color: '#334155' }}
|
||||
ellipsis={{ rows: 6 }}
|
||||
>
|
||||
<pre style={{ ...codeBlockStyle, whiteSpace: 'pre-wrap', margin: 0 }}>{jsonString}</pre>
|
||||
</Typography.Paragraph>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
const renderEvalResult = (rec: EvalItem) => {
|
||||
const r = rec.evalResult;
|
||||
let jsonString = '';
|
||||
try {
|
||||
if (typeof r === 'string') {
|
||||
try {
|
||||
jsonString = JSON.stringify(JSON.parse(r), null, 2);
|
||||
} catch {
|
||||
jsonString = JSON.stringify({ value: r, score: rec.evalScore ?? undefined }, null, 2);
|
||||
}
|
||||
} else {
|
||||
const withScore = rec.evalScore !== undefined && rec.evalScore !== null ? { ...r, evalScore: rec.evalScore } : r;
|
||||
jsonString = JSON.stringify(withScore, null, 2);
|
||||
}
|
||||
} catch {
|
||||
jsonString = 'null';
|
||||
}
|
||||
// 判空展示未评估
|
||||
const isEmpty = !r || (typeof r === 'string' && r.trim() === '') || (typeof r === 'object' && r !== null && Object.keys(r).length === 0);
|
||||
if (isEmpty) {
|
||||
return <Text type="secondary">未评估</Text>;
|
||||
}
|
||||
return (
|
||||
<Tooltip
|
||||
color="#fff"
|
||||
title={<pre style={{ ...codeBlockStyle, margin: 0, maxWidth: 800, whiteSpace: 'pre-wrap' }}>{jsonString}</pre>}
|
||||
overlayInnerStyle={{ maxHeight: 600, overflow: 'auto' }}
|
||||
>
|
||||
<Typography.Paragraph
|
||||
style={{ margin: 0, whiteSpace: 'pre-wrap', fontFamily: MONO_FONT, fontSize: 12, lineHeight: '20px', color: '#334155' }}
|
||||
ellipsis={{ rows: 6 }}
|
||||
>
|
||||
<pre style={{ ...codeBlockStyle, whiteSpace: 'pre-wrap', margin: 0 }}>{jsonString}</pre>
|
||||
</Typography.Paragraph>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
const itemColumns = [
|
||||
{
|
||||
title: '评估对象',
|
||||
dataIndex: 'evalContent',
|
||||
key: 'evalContent',
|
||||
render: (_: any, record: EvalItem) => renderEvalObject(record),
|
||||
width: COLUMN_WIDTH,
|
||||
},
|
||||
{
|
||||
title: '评估结果',
|
||||
dataIndex: 'evalResult',
|
||||
key: 'evalResult',
|
||||
render: (_: any, record: EvalItem) => renderEvalResult(record),
|
||||
width: COLUMN_WIDTH,
|
||||
},
|
||||
];
|
||||
|
||||
if (!task?.id) return <Empty description="任务不存在" />;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
{!selectedFile ? (
|
||||
<Table
|
||||
rowKey={(r: EvalFile) => r.fileId}
|
||||
columns={fileColumns}
|
||||
dataSource={files}
|
||||
loading={loadingFiles}
|
||||
size="middle"
|
||||
onRow={(record) => ({
|
||||
onClick: () => {
|
||||
setSelectedFile({ fileId: record.fileId, fileName: record.fileName });
|
||||
// 切换文件时,重置条目表到第一页
|
||||
setItemSearchParams((prev: any) => ({ ...prev, current: 1 }));
|
||||
},
|
||||
})}
|
||||
pagination={filePagination}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="sticky top-0 z-10 bg-white py-2" style={{ borderBottom: '1px solid #f0f0f0' }}>
|
||||
<Space wrap>
|
||||
<Button icon={<ArrowLeft size={16} />} onClick={() => { setSelectedFile(null); }}>
|
||||
返回文件列表
|
||||
</Button>
|
||||
<Space>
|
||||
<FileText size={16} />
|
||||
<Text strong>{selectedFile.fileName}</Text>
|
||||
<Text type="secondary">文件ID:{selectedFile.fileId}</Text>
|
||||
<Text type="secondary">共 {itemPagination.total} 条</Text>
|
||||
</Space>
|
||||
</Space>
|
||||
</div>
|
||||
<Table
|
||||
rowKey={(r: EvalItem) => r.id}
|
||||
columns={itemColumns}
|
||||
dataSource={items}
|
||||
loading={loadingItems}
|
||||
size="middle"
|
||||
pagination={itemPagination}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,132 +1,132 @@
|
||||
import { useState } from 'react';
|
||||
import { Descriptions, Empty, DescriptionsProps, Table, Button } from 'antd';
|
||||
import { EyeOutlined } from '@ant-design/icons';
|
||||
import PreviewPromptModal from "@/pages/DataEvaluation/Create/PreviewPrompt.tsx";
|
||||
import { formatDateTime } from "@/utils/unit.ts";
|
||||
import { evalTaskStatusMap, getEvalMethod, getEvalType, getSource } from "@/pages/DataEvaluation/evaluation.const.tsx";
|
||||
|
||||
const Overview = ({ task }) => {
|
||||
const [previewVisible, setPreviewVisible] = useState(false);
|
||||
if (!task) {
|
||||
return <Empty description="未找到评估任务信息" />;
|
||||
}
|
||||
|
||||
const generateEvaluationPrompt = () => {
|
||||
setPreviewVisible(true);
|
||||
};
|
||||
|
||||
// 基本信息
|
||||
const items: DescriptionsProps["items"] = [
|
||||
{
|
||||
key: "id",
|
||||
label: "ID",
|
||||
children: task.id,
|
||||
},
|
||||
{
|
||||
key: "name",
|
||||
label: "名称",
|
||||
children: task.name,
|
||||
},
|
||||
{
|
||||
key: "evalType",
|
||||
label: "评估类型",
|
||||
children: getEvalType(task.taskType),
|
||||
},
|
||||
{
|
||||
key: "evalMethod",
|
||||
label: "评估方式",
|
||||
children: getEvalMethod(task.evalMethod),
|
||||
},
|
||||
{
|
||||
key: "status",
|
||||
label: "状态",
|
||||
children: evalTaskStatusMap[task.status]?.label || "未知",
|
||||
},
|
||||
{
|
||||
key: "source",
|
||||
label: "评估数据",
|
||||
children: getSource(task.sourceType) + task.sourceName,
|
||||
},
|
||||
{
|
||||
key: "evalConfig.modelName",
|
||||
label: "模型",
|
||||
children: task.evalConfig?.modelName || task.evalConfig?.modelId,
|
||||
},
|
||||
{
|
||||
key: "createdBy",
|
||||
label: "创建者",
|
||||
children: task.createdBy || "未知",
|
||||
},
|
||||
{
|
||||
key: "createdAt",
|
||||
label: "创建时间",
|
||||
children: formatDateTime(task.createdAt),
|
||||
},
|
||||
{
|
||||
key: "updatedAt",
|
||||
label: "更新时间",
|
||||
children: formatDateTime(task.updatedAt),
|
||||
},
|
||||
{
|
||||
key: "description",
|
||||
label: "描述",
|
||||
children: task.description || "无",
|
||||
},
|
||||
];
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '维度',
|
||||
dataIndex: 'dimension',
|
||||
key: 'dimension',
|
||||
width: '30%',
|
||||
},
|
||||
{
|
||||
title: '描述',
|
||||
dataIndex: 'description',
|
||||
key: 'description',
|
||||
width: '60%',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className=" flex flex-col gap-4">
|
||||
{/* 基本信息 */}
|
||||
<Descriptions
|
||||
title="基本信息"
|
||||
layout="vertical"
|
||||
size="small"
|
||||
items={items}
|
||||
column={5}
|
||||
/>
|
||||
<h2 className="text-base font-semibold mt-8">评估维度</h2>
|
||||
<div className="overflow-x-auto">
|
||||
<Table
|
||||
size="middle"
|
||||
rowKey="id"
|
||||
columns={columns}
|
||||
dataSource={task?.evalConfig?.dimensions}
|
||||
scroll={{ x: "max-content", y: 600 }}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 p-3 bg-blue-50 rounded-lg border border-blue-200">
|
||||
<Button
|
||||
type="link"
|
||||
icon={<EyeOutlined />}
|
||||
onClick={generateEvaluationPrompt}
|
||||
>
|
||||
查看评估提示词
|
||||
</Button>
|
||||
</div>
|
||||
<PreviewPromptModal
|
||||
previewVisible={previewVisible}
|
||||
onCancel={() => setPreviewVisible(false)}
|
||||
evaluationPrompt={task?.evalPrompt}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Overview;
|
||||
import { useState } from 'react';
|
||||
import { Descriptions, Empty, DescriptionsProps, Table, Button } from 'antd';
|
||||
import { EyeOutlined } from '@ant-design/icons';
|
||||
import PreviewPromptModal from "@/pages/DataEvaluation/Create/PreviewPrompt.tsx";
|
||||
import { formatDateTime } from "@/utils/unit.ts";
|
||||
import { evalTaskStatusMap, getEvalMethod, getEvalType, getSource } from "@/pages/DataEvaluation/evaluation.const.tsx";
|
||||
|
||||
const Overview = ({ task }) => {
|
||||
const [previewVisible, setPreviewVisible] = useState(false);
|
||||
if (!task) {
|
||||
return <Empty description="未找到评估任务信息" />;
|
||||
}
|
||||
|
||||
const generateEvaluationPrompt = () => {
|
||||
setPreviewVisible(true);
|
||||
};
|
||||
|
||||
// 基本信息
|
||||
const items: DescriptionsProps["items"] = [
|
||||
{
|
||||
key: "id",
|
||||
label: "ID",
|
||||
children: task.id,
|
||||
},
|
||||
{
|
||||
key: "name",
|
||||
label: "名称",
|
||||
children: task.name,
|
||||
},
|
||||
{
|
||||
key: "evalType",
|
||||
label: "评估类型",
|
||||
children: getEvalType(task.taskType),
|
||||
},
|
||||
{
|
||||
key: "evalMethod",
|
||||
label: "评估方式",
|
||||
children: getEvalMethod(task.evalMethod),
|
||||
},
|
||||
{
|
||||
key: "status",
|
||||
label: "状态",
|
||||
children: evalTaskStatusMap[task.status]?.label || "未知",
|
||||
},
|
||||
{
|
||||
key: "source",
|
||||
label: "评估数据",
|
||||
children: getSource(task.sourceType) + task.sourceName,
|
||||
},
|
||||
{
|
||||
key: "evalConfig.modelName",
|
||||
label: "模型",
|
||||
children: task.evalConfig?.modelName || task.evalConfig?.modelId,
|
||||
},
|
||||
{
|
||||
key: "createdBy",
|
||||
label: "创建者",
|
||||
children: task.createdBy || "未知",
|
||||
},
|
||||
{
|
||||
key: "createdAt",
|
||||
label: "创建时间",
|
||||
children: formatDateTime(task.createdAt),
|
||||
},
|
||||
{
|
||||
key: "updatedAt",
|
||||
label: "更新时间",
|
||||
children: formatDateTime(task.updatedAt),
|
||||
},
|
||||
{
|
||||
key: "description",
|
||||
label: "描述",
|
||||
children: task.description || "无",
|
||||
},
|
||||
];
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '维度',
|
||||
dataIndex: 'dimension',
|
||||
key: 'dimension',
|
||||
width: '30%',
|
||||
},
|
||||
{
|
||||
title: '描述',
|
||||
dataIndex: 'description',
|
||||
key: 'description',
|
||||
width: '60%',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className=" flex flex-col gap-4">
|
||||
{/* 基本信息 */}
|
||||
<Descriptions
|
||||
title="基本信息"
|
||||
layout="vertical"
|
||||
size="small"
|
||||
items={items}
|
||||
column={5}
|
||||
/>
|
||||
<h2 className="text-base font-semibold mt-8">评估维度</h2>
|
||||
<div className="overflow-x-auto">
|
||||
<Table
|
||||
size="middle"
|
||||
rowKey="id"
|
||||
columns={columns}
|
||||
dataSource={task?.evalConfig?.dimensions}
|
||||
scroll={{ x: "max-content", y: 600 }}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 p-3 bg-blue-50 rounded-lg border border-blue-200">
|
||||
<Button
|
||||
type="link"
|
||||
icon={<EyeOutlined />}
|
||||
onClick={generateEvaluationPrompt}
|
||||
>
|
||||
查看评估提示词
|
||||
</Button>
|
||||
</div>
|
||||
<PreviewPromptModal
|
||||
previewVisible={previewVisible}
|
||||
onCancel={() => setPreviewVisible(false)}
|
||||
evaluationPrompt={task?.evalPrompt}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Overview;
|
||||
|
||||
@@ -1,407 +1,407 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { Button, Card, Badge, Input, Typography, Breadcrumb } from "antd";
|
||||
import {
|
||||
LeftOutlined,
|
||||
RightOutlined,
|
||||
SaveOutlined,
|
||||
ScissorOutlined,
|
||||
AimOutlined,
|
||||
CalendarOutlined,
|
||||
FileTextOutlined,
|
||||
StarFilled,
|
||||
DatabaseOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { mockTasks, presetEvaluationDimensions } from "@/mock/evaluation";
|
||||
import { useNavigate } from "react-router";
|
||||
import DetailHeader from "@/components/DetailHeader";
|
||||
|
||||
const { TextArea } = Input;
|
||||
const { Title } = Typography;
|
||||
|
||||
// 生成切片内容
|
||||
const generateSliceContent = (index: number) => {
|
||||
const contents = [
|
||||
"用户咨询产品退换货政策的相关问题,希望了解具体的退货流程和时间限制。客服详细解释了7天无理由退货政策,包括商品需要保持原包装完整的要求。这个回答涵盖了用户关心的主要问题,提供了明确的时间限制和条件说明。",
|
||||
"客服回复关于质量问题商品的处理方式,说明15天内免费换货服务,并承诺承担相关物流费用。用户对此表示满意,认为这个政策很合理。回答中明确区分了质量问题和非质量问题的不同处理方式。",
|
||||
"用户询问特殊商品的退换货政策,客服解释个人定制商品不支持退货的规定,并建议用户在购买前仔细确认商品信息。这个回答帮助用户理解了特殊商品的限制条件。",
|
||||
"关于退货流程的详细说明,客服介绍了在线申请退货的步骤,包括订单页面操作和快递上门取件服务。整个流程描述清晰,用户可以轻松按照步骤操作。",
|
||||
"用户对物流费用承担问题提出疑问,客服明确说明质量问题导致的退换货由公司承担物流费用,非质量问题由用户承担。这个回答消除了用户的疑虑。",
|
||||
];
|
||||
return contents[index % contents.length];
|
||||
};
|
||||
|
||||
const slices: EvaluationSlice[] = Array.from(
|
||||
{ length: mockTasks[0].sliceConfig?.sampleCount || 50 },
|
||||
(_, index) => ({
|
||||
id: `slice_${index + 1}`,
|
||||
content: generateSliceContent(index),
|
||||
sourceFile: `file_${Math.floor(index / 5) + 1}.txt`,
|
||||
sliceIndex: index % 5,
|
||||
sliceType: ["paragraph", "sentence", "semantic"][index % 3],
|
||||
metadata: {
|
||||
startPosition: index * 200,
|
||||
endPosition: (index + 1) * 200,
|
||||
pageNumber: Math.floor(index / 10) + 1,
|
||||
section: `Section ${Math.floor(index / 5) + 1}`,
|
||||
processingMethod: mockTasks[0].sliceConfig?.method || "语义分割",
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
const ManualEvaluatePage = () => {
|
||||
const navigate = useNavigate();
|
||||
const taskId = mockTasks[0].id;
|
||||
// 人工评估状态
|
||||
const [currentEvaluationTask, setCurrentEvaluationTask] =
|
||||
useState<EvaluationTask | null>(mockTasks[0]);
|
||||
const [evaluationSlices, setEvaluationSlices] =
|
||||
useState<EvaluationSlice[]>(slices);
|
||||
const [currentSliceIndex, setCurrentSliceIndex] = useState(0);
|
||||
const [sliceScores, setSliceScores] = useState<{
|
||||
[key: string]: { [dimensionId: string]: number };
|
||||
}>({});
|
||||
const [sliceComments, setSliceComments] = useState<{ [key: string]: string }>(
|
||||
{}
|
||||
);
|
||||
|
||||
const currentSlice = evaluationSlices[currentSliceIndex];
|
||||
const currentScores = sliceScores[currentSlice?.id] || {};
|
||||
const progress =
|
||||
evaluationSlices.length > 0
|
||||
? ((currentSliceIndex + 1) / evaluationSlices.length) * 100
|
||||
: 0;
|
||||
|
||||
// 获取任务的所有维度
|
||||
const getTaskAllDimensions = (task: EvaluationTask) => {
|
||||
const presetDimensions = presetEvaluationDimensions.filter((d) =>
|
||||
task.dimensions.includes(d.id)
|
||||
);
|
||||
return [...presetDimensions, ...(task.customDimensions || [])];
|
||||
};
|
||||
|
||||
const allDimensions = getTaskAllDimensions(mockTasks[0]);
|
||||
|
||||
// 更新切片评分
|
||||
const updateSliceScore = (
|
||||
sliceId: string,
|
||||
dimensionId: string,
|
||||
score: number
|
||||
) => {
|
||||
setSliceScores((prev) => ({
|
||||
...prev,
|
||||
[sliceId]: {
|
||||
...prev[sliceId],
|
||||
[dimensionId]: score,
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
// 保存当前切片评分并进入下一个
|
||||
const handleSaveAndNext = () => {
|
||||
const currentSlice = evaluationSlices[currentSliceIndex];
|
||||
if (!currentSlice) return;
|
||||
|
||||
// 检查是否所有维度都已评分
|
||||
const allDimensions = getTaskAllDimensions(currentEvaluationTask!);
|
||||
const currentScores = sliceScores[currentSlice.id] || {};
|
||||
const hasAllScores = allDimensions.every(
|
||||
(dim) => currentScores[dim.id] > 0
|
||||
);
|
||||
|
||||
if (!hasAllScores) {
|
||||
window.alert("请为所有维度评分后再保存");
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果是最后一个切片,完成评估
|
||||
if (currentSliceIndex === evaluationSlices.length - 1) {
|
||||
handleCompleteEvaluation();
|
||||
} else {
|
||||
setCurrentSliceIndex(currentSliceIndex + 1);
|
||||
}
|
||||
};
|
||||
|
||||
// 完成评估
|
||||
const handleCompleteEvaluation = () => {
|
||||
navigate(`/data/evaluation/task-report/${mockTasks[0].id}`);
|
||||
};
|
||||
|
||||
// 星星评分组件
|
||||
const StarRating = ({
|
||||
value,
|
||||
onChange,
|
||||
dimension,
|
||||
}: {
|
||||
value: number;
|
||||
onChange: (value: number) => void;
|
||||
dimension: EvaluationDimension;
|
||||
}) => {
|
||||
return (
|
||||
<div style={{ marginBottom: 8 }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<span style={{ fontWeight: 500 }}>{dimension.name}</span>
|
||||
<span style={{ fontSize: 13, color: "#888" }}>{value}/5</span>
|
||||
</div>
|
||||
<div style={{ fontSize: 12, color: "#888", marginBottom: 4 }}>
|
||||
{dimension.description}
|
||||
</div>
|
||||
<div>
|
||||
{[1, 2, 3, 4, 5].map((star) => (
|
||||
<Button
|
||||
key={star}
|
||||
type="text"
|
||||
icon={
|
||||
<StarFilled
|
||||
style={{
|
||||
color: star <= value ? "#fadb14" : "#d9d9d9",
|
||||
fontSize: 22,
|
||||
transition: "color 0.2s",
|
||||
}}
|
||||
/>
|
||||
}
|
||||
onClick={() => onChange(star)}
|
||||
style={{ padding: 0, marginRight: 2 }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 头部统计信息
|
||||
const statistics = [
|
||||
{
|
||||
icon: <DatabaseOutlined className="text-gray-500" />,
|
||||
label: "数据集",
|
||||
value: currentEvaluationTask?.datasetName || "",
|
||||
},
|
||||
{
|
||||
icon: <ScissorOutlined className="text-gray-500" />,
|
||||
label: "切片方法",
|
||||
value: currentEvaluationTask?.sliceConfig?.method || "",
|
||||
},
|
||||
{
|
||||
icon: <AimOutlined className="text-gray-500" />,
|
||||
label: "样本数量",
|
||||
value: evaluationSlices.length,
|
||||
},
|
||||
{
|
||||
icon: <CalendarOutlined className="text-gray-500" />,
|
||||
label: "创建时间",
|
||||
value: currentEvaluationTask?.createdAt || "",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<Breadcrumb
|
||||
items={[
|
||||
{
|
||||
title: (
|
||||
<span onClick={() => navigate("/data/evaluation")}>数据评估</span>
|
||||
),
|
||||
},
|
||||
{ title: "人工评估", key: "manual-evaluate" },
|
||||
]}
|
||||
/>
|
||||
{/* 头部信息 */}
|
||||
<DetailHeader
|
||||
data={{
|
||||
name: currentEvaluationTask?.name || "",
|
||||
description: "人工评估任务",
|
||||
icon: <FileTextOutlined />,
|
||||
createdAt: currentEvaluationTask?.createdAt,
|
||||
lastUpdated: currentEvaluationTask?.createdAt,
|
||||
}}
|
||||
statistics={statistics}
|
||||
operations={[]}
|
||||
/>
|
||||
{/* 进度条 */}
|
||||
<div className="flex justify-between items-center mt-4 mb-6">
|
||||
<div className="text-xs text-gray-500">
|
||||
当前进度: {currentSliceIndex + 1} / {evaluationSlices.length}
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="text-xs text-gray-500">
|
||||
{Math.round(progress)}% 完成
|
||||
</span>
|
||||
<div className="w-48 bg-gray-200 rounded h-2">
|
||||
<div
|
||||
className="bg-blue-600 h-2 rounded transition-all"
|
||||
style={{ width: `${progress}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-2xl font-bold text-blue-600">
|
||||
{Math.round(progress)}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{/* 左侧:切片内容 */}
|
||||
<Card>
|
||||
<div className="border-b border-gray-100 pb-4 mb-4 flex justify-between items-center">
|
||||
<span className="text-base font-semibold flex items-center gap-2">
|
||||
<FileTextOutlined />
|
||||
切片内容
|
||||
</span>
|
||||
<Badge
|
||||
count={`切片 ${currentSliceIndex + 1}`}
|
||||
style={{ background: "#fafafa", color: "#333" }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
{currentSlice && (
|
||||
<>
|
||||
{/* 切片元信息 */}
|
||||
<div className="bg-gray-50 rounded p-4 text-sm">
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<span className="text-gray-500">来源文件:</span>
|
||||
<span className="ml-2 font-medium">
|
||||
{currentSlice.sourceFile}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-500">处理方法:</span>
|
||||
<span className="ml-2 font-medium">
|
||||
{currentSlice.metadata.processingMethod}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-500">位置:</span>
|
||||
<span className="ml-2 font-medium">
|
||||
{currentSlice.metadata.startPosition}-
|
||||
{currentSlice.metadata.endPosition}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-500">章节:</span>
|
||||
<span className="ml-2 font-medium">
|
||||
{currentSlice.metadata.section}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 切片内容 */}
|
||||
<div className="border border-gray-100 rounded p-4 min-h-[180px]">
|
||||
<div className="text-xs text-gray-500 mb-2">内容预览</div>
|
||||
<div className="text-gray-900 leading-relaxed">
|
||||
{currentSlice.content}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 导航按钮 */}
|
||||
<div className="flex items-center justify-between border-t border-gray-100 pt-4 mt-2">
|
||||
<Button
|
||||
type="default"
|
||||
icon={<LeftOutlined />}
|
||||
onClick={() =>
|
||||
setCurrentSliceIndex(Math.max(0, currentSliceIndex - 1))
|
||||
}
|
||||
disabled={currentSliceIndex === 0}
|
||||
>
|
||||
上一个
|
||||
</Button>
|
||||
<span className="text-xs text-gray-500">
|
||||
{currentSliceIndex + 1} / {evaluationSlices.length}
|
||||
</span>
|
||||
<Button
|
||||
type="default"
|
||||
icon={<RightOutlined />}
|
||||
onClick={() =>
|
||||
setCurrentSliceIndex(
|
||||
Math.min(
|
||||
evaluationSlices.length - 1,
|
||||
currentSliceIndex + 1
|
||||
)
|
||||
)
|
||||
}
|
||||
disabled={currentSliceIndex === evaluationSlices.length - 1}
|
||||
>
|
||||
下一个
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* 右侧:评估维度 */}
|
||||
<Card>
|
||||
<div className="border-b border-gray-100 pb-4 mb-4">
|
||||
<span className="text-base font-semibold flex items-center gap-2">
|
||||
<StarFilled className="text-yellow-400" />
|
||||
评估维度
|
||||
</span>
|
||||
<div className="text-xs text-gray-500 mt-1">
|
||||
请为每个维度进行1-5星评分
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-4">
|
||||
{allDimensions.map((dimension) => (
|
||||
<div
|
||||
key={dimension.id}
|
||||
className="border border-gray-100 rounded p-4"
|
||||
>
|
||||
<StarRating
|
||||
value={currentScores[dimension.id] || 0}
|
||||
onChange={(score) =>
|
||||
updateSliceScore(
|
||||
currentSlice?.id || "",
|
||||
dimension.id,
|
||||
score
|
||||
)
|
||||
}
|
||||
dimension={dimension}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* 评论区域 */}
|
||||
<div className="border border-gray-100 rounded p-4">
|
||||
<span className="font-medium mb-2 block">评估备注</span>
|
||||
<TextArea
|
||||
placeholder="请输入对该切片的评估备注和建议..."
|
||||
value={sliceComments[currentSlice?.id || ""] || ""}
|
||||
onChange={(e) =>
|
||||
setSliceComments((prev) => ({
|
||||
...prev,
|
||||
[currentSlice?.id || ""]: e.target.value,
|
||||
}))
|
||||
}
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 保存按钮 */}
|
||||
<div className="border-t border-gray-100 pt-4">
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<SaveOutlined />}
|
||||
onClick={handleSaveAndNext}
|
||||
block
|
||||
size="large"
|
||||
>
|
||||
{currentSliceIndex === evaluationSlices.length - 1
|
||||
? "完成评估"
|
||||
: "保存并下一个"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ManualEvaluatePage;
|
||||
import { useState, useEffect } from "react";
|
||||
import { Button, Card, Badge, Input, Typography, Breadcrumb } from "antd";
|
||||
import {
|
||||
LeftOutlined,
|
||||
RightOutlined,
|
||||
SaveOutlined,
|
||||
ScissorOutlined,
|
||||
AimOutlined,
|
||||
CalendarOutlined,
|
||||
FileTextOutlined,
|
||||
StarFilled,
|
||||
DatabaseOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { mockTasks, presetEvaluationDimensions } from "@/mock/evaluation";
|
||||
import { useNavigate } from "react-router";
|
||||
import DetailHeader from "@/components/DetailHeader";
|
||||
|
||||
const { TextArea } = Input;
|
||||
const { Title } = Typography;
|
||||
|
||||
// 生成切片内容
|
||||
const generateSliceContent = (index: number) => {
|
||||
const contents = [
|
||||
"用户咨询产品退换货政策的相关问题,希望了解具体的退货流程和时间限制。客服详细解释了7天无理由退货政策,包括商品需要保持原包装完整的要求。这个回答涵盖了用户关心的主要问题,提供了明确的时间限制和条件说明。",
|
||||
"客服回复关于质量问题商品的处理方式,说明15天内免费换货服务,并承诺承担相关物流费用。用户对此表示满意,认为这个政策很合理。回答中明确区分了质量问题和非质量问题的不同处理方式。",
|
||||
"用户询问特殊商品的退换货政策,客服解释个人定制商品不支持退货的规定,并建议用户在购买前仔细确认商品信息。这个回答帮助用户理解了特殊商品的限制条件。",
|
||||
"关于退货流程的详细说明,客服介绍了在线申请退货的步骤,包括订单页面操作和快递上门取件服务。整个流程描述清晰,用户可以轻松按照步骤操作。",
|
||||
"用户对物流费用承担问题提出疑问,客服明确说明质量问题导致的退换货由公司承担物流费用,非质量问题由用户承担。这个回答消除了用户的疑虑。",
|
||||
];
|
||||
return contents[index % contents.length];
|
||||
};
|
||||
|
||||
const slices: EvaluationSlice[] = Array.from(
|
||||
{ length: mockTasks[0].sliceConfig?.sampleCount || 50 },
|
||||
(_, index) => ({
|
||||
id: `slice_${index + 1}`,
|
||||
content: generateSliceContent(index),
|
||||
sourceFile: `file_${Math.floor(index / 5) + 1}.txt`,
|
||||
sliceIndex: index % 5,
|
||||
sliceType: ["paragraph", "sentence", "semantic"][index % 3],
|
||||
metadata: {
|
||||
startPosition: index * 200,
|
||||
endPosition: (index + 1) * 200,
|
||||
pageNumber: Math.floor(index / 10) + 1,
|
||||
section: `Section ${Math.floor(index / 5) + 1}`,
|
||||
processingMethod: mockTasks[0].sliceConfig?.method || "语义分割",
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
const ManualEvaluatePage = () => {
|
||||
const navigate = useNavigate();
|
||||
const taskId = mockTasks[0].id;
|
||||
// 人工评估状态
|
||||
const [currentEvaluationTask, setCurrentEvaluationTask] =
|
||||
useState<EvaluationTask | null>(mockTasks[0]);
|
||||
const [evaluationSlices, setEvaluationSlices] =
|
||||
useState<EvaluationSlice[]>(slices);
|
||||
const [currentSliceIndex, setCurrentSliceIndex] = useState(0);
|
||||
const [sliceScores, setSliceScores] = useState<{
|
||||
[key: string]: { [dimensionId: string]: number };
|
||||
}>({});
|
||||
const [sliceComments, setSliceComments] = useState<{ [key: string]: string }>(
|
||||
{}
|
||||
);
|
||||
|
||||
const currentSlice = evaluationSlices[currentSliceIndex];
|
||||
const currentScores = sliceScores[currentSlice?.id] || {};
|
||||
const progress =
|
||||
evaluationSlices.length > 0
|
||||
? ((currentSliceIndex + 1) / evaluationSlices.length) * 100
|
||||
: 0;
|
||||
|
||||
// 获取任务的所有维度
|
||||
const getTaskAllDimensions = (task: EvaluationTask) => {
|
||||
const presetDimensions = presetEvaluationDimensions.filter((d) =>
|
||||
task.dimensions.includes(d.id)
|
||||
);
|
||||
return [...presetDimensions, ...(task.customDimensions || [])];
|
||||
};
|
||||
|
||||
const allDimensions = getTaskAllDimensions(mockTasks[0]);
|
||||
|
||||
// 更新切片评分
|
||||
const updateSliceScore = (
|
||||
sliceId: string,
|
||||
dimensionId: string,
|
||||
score: number
|
||||
) => {
|
||||
setSliceScores((prev) => ({
|
||||
...prev,
|
||||
[sliceId]: {
|
||||
...prev[sliceId],
|
||||
[dimensionId]: score,
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
// 保存当前切片评分并进入下一个
|
||||
const handleSaveAndNext = () => {
|
||||
const currentSlice = evaluationSlices[currentSliceIndex];
|
||||
if (!currentSlice) return;
|
||||
|
||||
// 检查是否所有维度都已评分
|
||||
const allDimensions = getTaskAllDimensions(currentEvaluationTask!);
|
||||
const currentScores = sliceScores[currentSlice.id] || {};
|
||||
const hasAllScores = allDimensions.every(
|
||||
(dim) => currentScores[dim.id] > 0
|
||||
);
|
||||
|
||||
if (!hasAllScores) {
|
||||
window.alert("请为所有维度评分后再保存");
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果是最后一个切片,完成评估
|
||||
if (currentSliceIndex === evaluationSlices.length - 1) {
|
||||
handleCompleteEvaluation();
|
||||
} else {
|
||||
setCurrentSliceIndex(currentSliceIndex + 1);
|
||||
}
|
||||
};
|
||||
|
||||
// 完成评估
|
||||
const handleCompleteEvaluation = () => {
|
||||
navigate(`/data/evaluation/task-report/${mockTasks[0].id}`);
|
||||
};
|
||||
|
||||
// 星星评分组件
|
||||
const StarRating = ({
|
||||
value,
|
||||
onChange,
|
||||
dimension,
|
||||
}: {
|
||||
value: number;
|
||||
onChange: (value: number) => void;
|
||||
dimension: EvaluationDimension;
|
||||
}) => {
|
||||
return (
|
||||
<div style={{ marginBottom: 8 }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<span style={{ fontWeight: 500 }}>{dimension.name}</span>
|
||||
<span style={{ fontSize: 13, color: "#888" }}>{value}/5</span>
|
||||
</div>
|
||||
<div style={{ fontSize: 12, color: "#888", marginBottom: 4 }}>
|
||||
{dimension.description}
|
||||
</div>
|
||||
<div>
|
||||
{[1, 2, 3, 4, 5].map((star) => (
|
||||
<Button
|
||||
key={star}
|
||||
type="text"
|
||||
icon={
|
||||
<StarFilled
|
||||
style={{
|
||||
color: star <= value ? "#fadb14" : "#d9d9d9",
|
||||
fontSize: 22,
|
||||
transition: "color 0.2s",
|
||||
}}
|
||||
/>
|
||||
}
|
||||
onClick={() => onChange(star)}
|
||||
style={{ padding: 0, marginRight: 2 }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 头部统计信息
|
||||
const statistics = [
|
||||
{
|
||||
icon: <DatabaseOutlined className="text-gray-500" />,
|
||||
label: "数据集",
|
||||
value: currentEvaluationTask?.datasetName || "",
|
||||
},
|
||||
{
|
||||
icon: <ScissorOutlined className="text-gray-500" />,
|
||||
label: "切片方法",
|
||||
value: currentEvaluationTask?.sliceConfig?.method || "",
|
||||
},
|
||||
{
|
||||
icon: <AimOutlined className="text-gray-500" />,
|
||||
label: "样本数量",
|
||||
value: evaluationSlices.length,
|
||||
},
|
||||
{
|
||||
icon: <CalendarOutlined className="text-gray-500" />,
|
||||
label: "创建时间",
|
||||
value: currentEvaluationTask?.createdAt || "",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<Breadcrumb
|
||||
items={[
|
||||
{
|
||||
title: (
|
||||
<span onClick={() => navigate("/data/evaluation")}>数据评估</span>
|
||||
),
|
||||
},
|
||||
{ title: "人工评估", key: "manual-evaluate" },
|
||||
]}
|
||||
/>
|
||||
{/* 头部信息 */}
|
||||
<DetailHeader
|
||||
data={{
|
||||
name: currentEvaluationTask?.name || "",
|
||||
description: "人工评估任务",
|
||||
icon: <FileTextOutlined />,
|
||||
createdAt: currentEvaluationTask?.createdAt,
|
||||
lastUpdated: currentEvaluationTask?.createdAt,
|
||||
}}
|
||||
statistics={statistics}
|
||||
operations={[]}
|
||||
/>
|
||||
{/* 进度条 */}
|
||||
<div className="flex justify-between items-center mt-4 mb-6">
|
||||
<div className="text-xs text-gray-500">
|
||||
当前进度: {currentSliceIndex + 1} / {evaluationSlices.length}
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="text-xs text-gray-500">
|
||||
{Math.round(progress)}% 完成
|
||||
</span>
|
||||
<div className="w-48 bg-gray-200 rounded h-2">
|
||||
<div
|
||||
className="bg-blue-600 h-2 rounded transition-all"
|
||||
style={{ width: `${progress}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-2xl font-bold text-blue-600">
|
||||
{Math.round(progress)}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{/* 左侧:切片内容 */}
|
||||
<Card>
|
||||
<div className="border-b border-gray-100 pb-4 mb-4 flex justify-between items-center">
|
||||
<span className="text-base font-semibold flex items-center gap-2">
|
||||
<FileTextOutlined />
|
||||
切片内容
|
||||
</span>
|
||||
<Badge
|
||||
count={`切片 ${currentSliceIndex + 1}`}
|
||||
style={{ background: "#fafafa", color: "#333" }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
{currentSlice && (
|
||||
<>
|
||||
{/* 切片元信息 */}
|
||||
<div className="bg-gray-50 rounded p-4 text-sm">
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<span className="text-gray-500">来源文件:</span>
|
||||
<span className="ml-2 font-medium">
|
||||
{currentSlice.sourceFile}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-500">处理方法:</span>
|
||||
<span className="ml-2 font-medium">
|
||||
{currentSlice.metadata.processingMethod}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-500">位置:</span>
|
||||
<span className="ml-2 font-medium">
|
||||
{currentSlice.metadata.startPosition}-
|
||||
{currentSlice.metadata.endPosition}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-500">章节:</span>
|
||||
<span className="ml-2 font-medium">
|
||||
{currentSlice.metadata.section}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 切片内容 */}
|
||||
<div className="border border-gray-100 rounded p-4 min-h-[180px]">
|
||||
<div className="text-xs text-gray-500 mb-2">内容预览</div>
|
||||
<div className="text-gray-900 leading-relaxed">
|
||||
{currentSlice.content}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 导航按钮 */}
|
||||
<div className="flex items-center justify-between border-t border-gray-100 pt-4 mt-2">
|
||||
<Button
|
||||
type="default"
|
||||
icon={<LeftOutlined />}
|
||||
onClick={() =>
|
||||
setCurrentSliceIndex(Math.max(0, currentSliceIndex - 1))
|
||||
}
|
||||
disabled={currentSliceIndex === 0}
|
||||
>
|
||||
上一个
|
||||
</Button>
|
||||
<span className="text-xs text-gray-500">
|
||||
{currentSliceIndex + 1} / {evaluationSlices.length}
|
||||
</span>
|
||||
<Button
|
||||
type="default"
|
||||
icon={<RightOutlined />}
|
||||
onClick={() =>
|
||||
setCurrentSliceIndex(
|
||||
Math.min(
|
||||
evaluationSlices.length - 1,
|
||||
currentSliceIndex + 1
|
||||
)
|
||||
)
|
||||
}
|
||||
disabled={currentSliceIndex === evaluationSlices.length - 1}
|
||||
>
|
||||
下一个
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* 右侧:评估维度 */}
|
||||
<Card>
|
||||
<div className="border-b border-gray-100 pb-4 mb-4">
|
||||
<span className="text-base font-semibold flex items-center gap-2">
|
||||
<StarFilled className="text-yellow-400" />
|
||||
评估维度
|
||||
</span>
|
||||
<div className="text-xs text-gray-500 mt-1">
|
||||
请为每个维度进行1-5星评分
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-4">
|
||||
{allDimensions.map((dimension) => (
|
||||
<div
|
||||
key={dimension.id}
|
||||
className="border border-gray-100 rounded p-4"
|
||||
>
|
||||
<StarRating
|
||||
value={currentScores[dimension.id] || 0}
|
||||
onChange={(score) =>
|
||||
updateSliceScore(
|
||||
currentSlice?.id || "",
|
||||
dimension.id,
|
||||
score
|
||||
)
|
||||
}
|
||||
dimension={dimension}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* 评论区域 */}
|
||||
<div className="border border-gray-100 rounded p-4">
|
||||
<span className="font-medium mb-2 block">评估备注</span>
|
||||
<TextArea
|
||||
placeholder="请输入对该切片的评估备注和建议..."
|
||||
value={sliceComments[currentSlice?.id || ""] || ""}
|
||||
onChange={(e) =>
|
||||
setSliceComments((prev) => ({
|
||||
...prev,
|
||||
[currentSlice?.id || ""]: e.target.value,
|
||||
}))
|
||||
}
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 保存按钮 */}
|
||||
<div className="border-t border-gray-100 pt-4">
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<SaveOutlined />}
|
||||
onClick={handleSaveAndNext}
|
||||
block
|
||||
size="large"
|
||||
>
|
||||
{currentSliceIndex === evaluationSlices.length - 1
|
||||
? "完成评估"
|
||||
: "保存并下一个"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ManualEvaluatePage;
|
||||
|
||||
@@ -1,267 +1,267 @@
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Table,
|
||||
Tag,
|
||||
Space,
|
||||
Typography,
|
||||
Progress,
|
||||
Popconfirm,
|
||||
App,
|
||||
} from "antd";
|
||||
import {
|
||||
PlusOutlined,
|
||||
DeleteOutlined
|
||||
} from "@ant-design/icons";
|
||||
import { SearchControls } from "@/components/SearchControls";
|
||||
import { useNavigate } from "react-router";
|
||||
import { deleteEvaluationTaskUsingGet, getPagedEvaluationTaskUsingGet } from "@/pages/DataEvaluation/evaluation.api";
|
||||
import CardView from "@/components/CardView";
|
||||
import CreateTaskModal from "@/pages/DataEvaluation/Create/CreateTask.tsx";
|
||||
import useFetchData from "@/hooks/useFetchData.ts";
|
||||
import { EvaluationTask } from "@/pages/DataEvaluation/evaluation.model.ts";
|
||||
import { mapEvaluationTask } from "@/pages/DataEvaluation/evaluation.const.tsx";
|
||||
|
||||
const { Text, Title } = Typography;
|
||||
|
||||
const statusMap = {
|
||||
PENDING: { text: '等待中', color: 'warning'},
|
||||
RUNNING: { text: '运行中', color: 'processing'},
|
||||
COMPLETED: { text: '已完成', color: 'success'},
|
||||
STOPPED: { text: '已停止', color: 'default'},
|
||||
FAILED: { text: '失败', color: 'error'},
|
||||
};
|
||||
|
||||
export default function DataEvaluationPage() {
|
||||
const { message } = App.useApp();
|
||||
const navigate = useNavigate();
|
||||
const [viewMode, setViewMode] = useState<"card" | "list">("list");
|
||||
const {
|
||||
loading,
|
||||
tableData,
|
||||
pagination,
|
||||
searchParams,
|
||||
setSearchParams,
|
||||
handleFiltersChange,
|
||||
fetchData,
|
||||
} = useFetchData<EvaluationTask>(
|
||||
getPagedEvaluationTaskUsingGet,
|
||||
mapEvaluationTask,
|
||||
30000,
|
||||
true,
|
||||
[],
|
||||
0
|
||||
);
|
||||
|
||||
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||
|
||||
const handleDeleteTask = async (task: EvaluationTask) => {
|
||||
try {
|
||||
// 调用删除接口
|
||||
await deleteEvaluationTaskUsingGet(task.id);
|
||||
message.success("任务删除成功");
|
||||
// 重新加载数据
|
||||
fetchData().then();
|
||||
} catch (error) {
|
||||
message.error("任务删除失败,请稍后重试");
|
||||
}
|
||||
};
|
||||
|
||||
const filterOptions = [
|
||||
{
|
||||
key: 'status',
|
||||
label: '状态',
|
||||
options: Object.entries(statusMap).map(([value, { text }]) => ({
|
||||
value,
|
||||
label: text,
|
||||
})),
|
||||
},
|
||||
{
|
||||
key: 'taskType',
|
||||
label: '任务类型',
|
||||
options: [
|
||||
{ value: 'QA', label: 'QA评估' },
|
||||
{ value: 'COT', label: 'COPT评估' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'evalMethod',
|
||||
label: '评估方式',
|
||||
options: [
|
||||
{ value: 'AUTO', label: '自动评估' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '任务名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
render: (name, record) => (
|
||||
<Button
|
||||
type="link"
|
||||
onClick={() => navigate(`/data/evaluation/detail/${record.id}`)}
|
||||
>
|
||||
{name}
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '任务类型',
|
||||
dataIndex: 'taskType',
|
||||
key: 'taskType',
|
||||
render: (text: string) => (
|
||||
<Tag color={text === 'QA' ? 'blue' : 'default'}>
|
||||
{text === 'QA' ? 'QA评估' : text}
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '评估方式',
|
||||
dataIndex: 'evalMethod',
|
||||
key: 'evalMethod',
|
||||
render: (text: string) => (
|
||||
<Tag color={text === 'AUTO' ? 'geekblue' : 'orange'}>
|
||||
{text === 'AUTO' ? '自动评估' : '人工评估'}
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
render: (status: any) => {
|
||||
return (<Tag color={status.color}> {status.label} </Tag>);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '进度',
|
||||
dataIndex: 'evalProcess',
|
||||
key: 'evalProcess',
|
||||
render: (progress: number, record: EvaluationTask) => (
|
||||
<Progress
|
||||
percent={Math.round(progress * 100)}
|
||||
size="small"
|
||||
status={record.status === 'FAILED' ? 'exception' : 'active'}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "创建时间",
|
||||
dataIndex: "updatedAt",
|
||||
key: "updatedAt",
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
render: (_: any, task: EvaluationTask) => (
|
||||
<div className="flex items-center gap-2">
|
||||
{operations.map((op) => {
|
||||
if (op.confirm) {
|
||||
<Popconfirm
|
||||
title={op.confirm.title}
|
||||
description={op.confirm.description}
|
||||
onConfirm={() => op.onClick(task)}
|
||||
>
|
||||
<Button type="text" icon={op.icon} />
|
||||
</Popconfirm>;
|
||||
}
|
||||
return (
|
||||
<Button
|
||||
key={op.key}
|
||||
type="text"
|
||||
icon={op.icon}
|
||||
danger={op.danger}
|
||||
onClick={() => op.onClick(task)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const operations = [
|
||||
{
|
||||
key: "delete",
|
||||
label: "删除",
|
||||
danger: true,
|
||||
confirm: {
|
||||
title: "确认删除该任务?",
|
||||
description: "删除后该任务将无法恢复,请谨慎操作。",
|
||||
okText: "删除",
|
||||
cancelText: "取消",
|
||||
okType: "danger",
|
||||
},
|
||||
icon: <DeleteOutlined />,
|
||||
onClick: handleDeleteTask,
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="p-4">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<Title level={4} style={{ margin: 0 }}>数据评估</Title>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => setIsModalVisible(true)}
|
||||
>
|
||||
创建评估任务
|
||||
</Button>
|
||||
</div>
|
||||
<>
|
||||
{/* 搜索、筛选和视图控制 */}
|
||||
<SearchControls
|
||||
searchTerm={searchParams.keyword}
|
||||
onSearchChange={(keyword) =>
|
||||
setSearchParams({ ...searchParams, keyword })
|
||||
}
|
||||
searchPlaceholder="搜索任务名称..."
|
||||
filters={filterOptions}
|
||||
onFiltersChange={handleFiltersChange}
|
||||
onClearFilters={() =>
|
||||
setSearchParams({ ...searchParams, filter: {} })
|
||||
}
|
||||
viewMode={viewMode}
|
||||
onViewModeChange={setViewMode}
|
||||
showViewToggle
|
||||
onReload={fetchData}
|
||||
/>
|
||||
{/* 任务列表 */}
|
||||
{viewMode === "list" ? (
|
||||
<Card>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={tableData}
|
||||
pagination={pagination}
|
||||
rowKey="id"
|
||||
scroll={{ x: "max-content", y: "calc(100vh - 30rem)" }}
|
||||
/>
|
||||
</Card>
|
||||
) : (
|
||||
<CardView
|
||||
loading={loading}
|
||||
data={tableData}
|
||||
operations={operations}
|
||||
pagination={pagination}
|
||||
onView={(task) => {
|
||||
navigate(`/data/evaluation/detail/${task.id}`);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
<CreateTaskModal
|
||||
visible={isModalVisible}
|
||||
onCancel={() => setIsModalVisible(false)}
|
||||
onSuccess={() => {
|
||||
setIsModalVisible(false);
|
||||
fetchData();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Table,
|
||||
Tag,
|
||||
Space,
|
||||
Typography,
|
||||
Progress,
|
||||
Popconfirm,
|
||||
App,
|
||||
} from "antd";
|
||||
import {
|
||||
PlusOutlined,
|
||||
DeleteOutlined
|
||||
} from "@ant-design/icons";
|
||||
import { SearchControls } from "@/components/SearchControls";
|
||||
import { useNavigate } from "react-router";
|
||||
import { deleteEvaluationTaskUsingGet, getPagedEvaluationTaskUsingGet } from "@/pages/DataEvaluation/evaluation.api";
|
||||
import CardView from "@/components/CardView";
|
||||
import CreateTaskModal from "@/pages/DataEvaluation/Create/CreateTask.tsx";
|
||||
import useFetchData from "@/hooks/useFetchData.ts";
|
||||
import { EvaluationTask } from "@/pages/DataEvaluation/evaluation.model.ts";
|
||||
import { mapEvaluationTask } from "@/pages/DataEvaluation/evaluation.const.tsx";
|
||||
|
||||
const { Text, Title } = Typography;
|
||||
|
||||
const statusMap = {
|
||||
PENDING: { text: '等待中', color: 'warning'},
|
||||
RUNNING: { text: '运行中', color: 'processing'},
|
||||
COMPLETED: { text: '已完成', color: 'success'},
|
||||
STOPPED: { text: '已停止', color: 'default'},
|
||||
FAILED: { text: '失败', color: 'error'},
|
||||
};
|
||||
|
||||
export default function DataEvaluationPage() {
|
||||
const { message } = App.useApp();
|
||||
const navigate = useNavigate();
|
||||
const [viewMode, setViewMode] = useState<"card" | "list">("list");
|
||||
const {
|
||||
loading,
|
||||
tableData,
|
||||
pagination,
|
||||
searchParams,
|
||||
setSearchParams,
|
||||
handleFiltersChange,
|
||||
fetchData,
|
||||
} = useFetchData<EvaluationTask>(
|
||||
getPagedEvaluationTaskUsingGet,
|
||||
mapEvaluationTask,
|
||||
30000,
|
||||
true,
|
||||
[],
|
||||
0
|
||||
);
|
||||
|
||||
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||
|
||||
const handleDeleteTask = async (task: EvaluationTask) => {
|
||||
try {
|
||||
// 调用删除接口
|
||||
await deleteEvaluationTaskUsingGet(task.id);
|
||||
message.success("任务删除成功");
|
||||
// 重新加载数据
|
||||
fetchData().then();
|
||||
} catch (error) {
|
||||
message.error("任务删除失败,请稍后重试");
|
||||
}
|
||||
};
|
||||
|
||||
const filterOptions = [
|
||||
{
|
||||
key: 'status',
|
||||
label: '状态',
|
||||
options: Object.entries(statusMap).map(([value, { text }]) => ({
|
||||
value,
|
||||
label: text,
|
||||
})),
|
||||
},
|
||||
{
|
||||
key: 'taskType',
|
||||
label: '任务类型',
|
||||
options: [
|
||||
{ value: 'QA', label: 'QA评估' },
|
||||
{ value: 'COT', label: 'COPT评估' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'evalMethod',
|
||||
label: '评估方式',
|
||||
options: [
|
||||
{ value: 'AUTO', label: '自动评估' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '任务名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
render: (name, record) => (
|
||||
<Button
|
||||
type="link"
|
||||
onClick={() => navigate(`/data/evaluation/detail/${record.id}`)}
|
||||
>
|
||||
{name}
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '任务类型',
|
||||
dataIndex: 'taskType',
|
||||
key: 'taskType',
|
||||
render: (text: string) => (
|
||||
<Tag color={text === 'QA' ? 'blue' : 'default'}>
|
||||
{text === 'QA' ? 'QA评估' : text}
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '评估方式',
|
||||
dataIndex: 'evalMethod',
|
||||
key: 'evalMethod',
|
||||
render: (text: string) => (
|
||||
<Tag color={text === 'AUTO' ? 'geekblue' : 'orange'}>
|
||||
{text === 'AUTO' ? '自动评估' : '人工评估'}
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
render: (status: any) => {
|
||||
return (<Tag color={status.color}> {status.label} </Tag>);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '进度',
|
||||
dataIndex: 'evalProcess',
|
||||
key: 'evalProcess',
|
||||
render: (progress: number, record: EvaluationTask) => (
|
||||
<Progress
|
||||
percent={Math.round(progress * 100)}
|
||||
size="small"
|
||||
status={record.status === 'FAILED' ? 'exception' : 'active'}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "创建时间",
|
||||
dataIndex: "updatedAt",
|
||||
key: "updatedAt",
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
render: (_: any, task: EvaluationTask) => (
|
||||
<div className="flex items-center gap-2">
|
||||
{operations.map((op) => {
|
||||
if (op.confirm) {
|
||||
<Popconfirm
|
||||
title={op.confirm.title}
|
||||
description={op.confirm.description}
|
||||
onConfirm={() => op.onClick(task)}
|
||||
>
|
||||
<Button type="text" icon={op.icon} />
|
||||
</Popconfirm>;
|
||||
}
|
||||
return (
|
||||
<Button
|
||||
key={op.key}
|
||||
type="text"
|
||||
icon={op.icon}
|
||||
danger={op.danger}
|
||||
onClick={() => op.onClick(task)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const operations = [
|
||||
{
|
||||
key: "delete",
|
||||
label: "删除",
|
||||
danger: true,
|
||||
confirm: {
|
||||
title: "确认删除该任务?",
|
||||
description: "删除后该任务将无法恢复,请谨慎操作。",
|
||||
okText: "删除",
|
||||
cancelText: "取消",
|
||||
okType: "danger",
|
||||
},
|
||||
icon: <DeleteOutlined />,
|
||||
onClick: handleDeleteTask,
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="p-4">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<Title level={4} style={{ margin: 0 }}>数据评估</Title>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => setIsModalVisible(true)}
|
||||
>
|
||||
创建评估任务
|
||||
</Button>
|
||||
</div>
|
||||
<>
|
||||
{/* 搜索、筛选和视图控制 */}
|
||||
<SearchControls
|
||||
searchTerm={searchParams.keyword}
|
||||
onSearchChange={(keyword) =>
|
||||
setSearchParams({ ...searchParams, keyword })
|
||||
}
|
||||
searchPlaceholder="搜索任务名称..."
|
||||
filters={filterOptions}
|
||||
onFiltersChange={handleFiltersChange}
|
||||
onClearFilters={() =>
|
||||
setSearchParams({ ...searchParams, filter: {} })
|
||||
}
|
||||
viewMode={viewMode}
|
||||
onViewModeChange={setViewMode}
|
||||
showViewToggle
|
||||
onReload={fetchData}
|
||||
/>
|
||||
{/* 任务列表 */}
|
||||
{viewMode === "list" ? (
|
||||
<Card>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={tableData}
|
||||
pagination={pagination}
|
||||
rowKey="id"
|
||||
scroll={{ x: "max-content", y: "calc(100vh - 30rem)" }}
|
||||
/>
|
||||
</Card>
|
||||
) : (
|
||||
<CardView
|
||||
loading={loading}
|
||||
data={tableData}
|
||||
operations={operations}
|
||||
pagination={pagination}
|
||||
onView={(task) => {
|
||||
navigate(`/data/evaluation/detail/${task.id}`);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
<CreateTaskModal
|
||||
visible={isModalVisible}
|
||||
onCancel={() => setIsModalVisible(false)}
|
||||
onSuccess={() => {
|
||||
setIsModalVisible(false);
|
||||
fetchData();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,308 +1,308 @@
|
||||
import { Button, Card, Badge, Breadcrumb } from "antd";
|
||||
import {
|
||||
Download,
|
||||
Users,
|
||||
Scissors,
|
||||
BarChart3,
|
||||
Target,
|
||||
Calendar,
|
||||
TrendingUp,
|
||||
MessageSquare,
|
||||
Star,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
mockQAPairs,
|
||||
mockTasks,
|
||||
presetEvaluationDimensions,
|
||||
} from "@/mock/evaluation";
|
||||
import { Link } from "react-router";
|
||||
|
||||
const EvaluationTaskReport = () => {
|
||||
// const navigate = useNavigate();
|
||||
const selectedTask = mockTasks[0]; // 假设我们只展示第一个任务的报告
|
||||
|
||||
// 获取任务的所有维度
|
||||
const getTaskAllDimensions = (task: EvaluationTask) => {
|
||||
const presetDimensions = presetEvaluationDimensions.filter((d) =>
|
||||
task.dimensions.includes(d.id)
|
||||
);
|
||||
return [...presetDimensions, ...(task.customDimensions || [])];
|
||||
};
|
||||
const allDimensions = getTaskAllDimensions(selectedTask);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen">
|
||||
<div className="mx-auto space-y-2">
|
||||
{/* 头部 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<Breadcrumb
|
||||
items={[
|
||||
{
|
||||
title: <Link to="/data/evaluation">数据评估</Link>,
|
||||
},
|
||||
{ title: "评估报告", key: "report" },
|
||||
]}
|
||||
></Breadcrumb>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
className="flex items-center gap-2"
|
||||
icon={<Download className="w-4 h-4" />}
|
||||
>
|
||||
导出报告
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 基本信息卡片 */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<Card>
|
||||
<div className="p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 bg-blue-100 rounded-lg">
|
||||
<BarChart3 className="w-5 h-5 text-blue-600" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-2xl font-bold text-gray-900">
|
||||
{selectedTask.score || 0}
|
||||
</div>
|
||||
<div className="text-sm text-gray-500">总体评分</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<div className="p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 bg-green-100 rounded-lg">
|
||||
<Target className="w-5 h-5 text-green-600" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-2xl font-bold text-gray-900">
|
||||
{selectedTask.sliceConfig?.sampleCount}
|
||||
</div>
|
||||
<div className="text-sm text-gray-500">评估样本数</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<div className="p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 bg-purple-100 rounded-lg">
|
||||
<Users className="w-5 h-5 text-purple-600" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-2xl font-bold text-gray-900">
|
||||
{selectedTask.evaluationType === "manual" ? "人工" : "模型"}
|
||||
</div>
|
||||
<div className="text-sm text-gray-500">评估方式</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<div className="p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 bg-orange-100 rounded-lg">
|
||||
<Calendar className="w-5 h-5 text-orange-600" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-2xl font-bold text-gray-900">
|
||||
{selectedTask.progress}%
|
||||
</div>
|
||||
<div className="text-sm text-gray-500">完成进度</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 详细信息 */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* 评估结果 */}
|
||||
<Card
|
||||
title={
|
||||
<span className="flex items-center gap-2">
|
||||
<TrendingUp className="w-5 h-5" />
|
||||
评估结果
|
||||
</span>
|
||||
}
|
||||
styles={{ body: { paddingTop: 0 } }}
|
||||
>
|
||||
{/* 维度评分 */}
|
||||
<div className="mt-4">
|
||||
<h4 className="font-medium mb-3">维度评分</h4>
|
||||
<div className="space-y-3">
|
||||
{allDimensions.map((dimension) => {
|
||||
const score = 75 + Math.floor(Math.random() * 20); // 模拟评分
|
||||
return (
|
||||
<div
|
||||
key={dimension.id}
|
||||
className="flex items-center justify-between"
|
||||
>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className="text-sm font-medium">
|
||||
{dimension.name}
|
||||
</span>
|
||||
<span className="text-sm font-bold text-blue-600">
|
||||
{score}分
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-full bg-gray-200 rounded-full h-2">
|
||||
<div
|
||||
className="bg-blue-500 h-2 rounded-full"
|
||||
style={{ width: `${score}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 质量分数解读 */}
|
||||
<div className="border-t pt-4 mt-4">
|
||||
<h4 className="font-medium mb-3">质量分数解读</h4>
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-3 h-3 bg-green-500 rounded-full" />
|
||||
<span>90-100分: 优秀,质量很高</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-3 h-3 bg-blue-500 rounded-full" />
|
||||
<span>80-89分: 良好,质量较好</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-3 h-3 bg-yellow-500 rounded-full" />
|
||||
<span>70-79分: 一般,需要改进</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-3 h-3 bg-red-500 rounded-full" />
|
||||
<span>60-69分: 较差,需要重点关注</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* 切片信息 */}
|
||||
<Card
|
||||
title={
|
||||
<span className="flex items-center gap-2">
|
||||
<Scissors className="w-5 h-5" />
|
||||
切片信息
|
||||
</span>
|
||||
}
|
||||
styles={{ body: { paddingTop: 0 } }}
|
||||
>
|
||||
<div className="space-y-4 mt-4">
|
||||
<div className="grid grid-cols-2 gap-4 text-sm">
|
||||
<div>
|
||||
<span className="text-gray-500">切片阈值:</span>
|
||||
<span className="ml-2 font-medium">
|
||||
{selectedTask.sliceConfig?.threshold}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-500">抽样数量:</span>
|
||||
<span className="ml-2 font-medium">
|
||||
{selectedTask.sliceConfig?.sampleCount}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-500">切片方法:</span>
|
||||
<span className="ml-2 font-medium">
|
||||
{selectedTask.sliceConfig?.method}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-500">评估时间:</span>
|
||||
<span className="ml-2 font-medium">
|
||||
{selectedTask.completedAt || selectedTask.createdAt}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t pt-4">
|
||||
<h4 className="font-medium mb-3">评估维度</h4>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{allDimensions.map((dimension) => (
|
||||
<Badge
|
||||
key={dimension.id}
|
||||
style={{
|
||||
border: "1px solid #d9d9d9",
|
||||
background: "#fafafa",
|
||||
padding: "0 8px",
|
||||
}}
|
||||
>
|
||||
{dimension.name}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* QA对详情 */}
|
||||
<Card
|
||||
title={
|
||||
<span className="flex items-center gap-2">
|
||||
<MessageSquare className="w-5 h-5" />
|
||||
QA对详情
|
||||
</span>
|
||||
}
|
||||
styles={{ body: { paddingTop: 0 } }}
|
||||
>
|
||||
<div className="space-y-4 mt-4">
|
||||
{mockQAPairs.map((qa) => (
|
||||
<div key={qa.id} className="border rounded-lg p-4">
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<span className="text-sm font-medium text-gray-700 mb-1">
|
||||
问题:
|
||||
</span>
|
||||
<span className="text-gray-900">{qa.question}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-sm font-medium text-gray-700 mb-1">
|
||||
回答:
|
||||
</span>
|
||||
<span className="text-gray-900">{qa.answer}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between pt-2 border-t">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-gray-500">评分:</span>
|
||||
<div className="flex items-center gap-1">
|
||||
{[1, 2, 3, 4, 5].map((star) => (
|
||||
<Star
|
||||
key={star}
|
||||
className={`w-4 h-4 ${star <= qa.score
|
||||
? "text-yellow-400"
|
||||
: "text-gray-300"
|
||||
}`}
|
||||
style={star <= qa.score ? { fill: "#facc15" } : {}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<span className="text-sm font-medium">{qa.score}/5</span>
|
||||
</div>
|
||||
{qa.feedback && (
|
||||
<div className="text-sm text-gray-600">{qa.feedback}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EvaluationTaskReport;
|
||||
import { Button, Card, Badge, Breadcrumb } from "antd";
|
||||
import {
|
||||
Download,
|
||||
Users,
|
||||
Scissors,
|
||||
BarChart3,
|
||||
Target,
|
||||
Calendar,
|
||||
TrendingUp,
|
||||
MessageSquare,
|
||||
Star,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
mockQAPairs,
|
||||
mockTasks,
|
||||
presetEvaluationDimensions,
|
||||
} from "@/mock/evaluation";
|
||||
import { Link } from "react-router";
|
||||
|
||||
const EvaluationTaskReport = () => {
|
||||
// const navigate = useNavigate();
|
||||
const selectedTask = mockTasks[0]; // 假设我们只展示第一个任务的报告
|
||||
|
||||
// 获取任务的所有维度
|
||||
const getTaskAllDimensions = (task: EvaluationTask) => {
|
||||
const presetDimensions = presetEvaluationDimensions.filter((d) =>
|
||||
task.dimensions.includes(d.id)
|
||||
);
|
||||
return [...presetDimensions, ...(task.customDimensions || [])];
|
||||
};
|
||||
const allDimensions = getTaskAllDimensions(selectedTask);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen">
|
||||
<div className="mx-auto space-y-2">
|
||||
{/* 头部 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<Breadcrumb
|
||||
items={[
|
||||
{
|
||||
title: <Link to="/data/evaluation">数据评估</Link>,
|
||||
},
|
||||
{ title: "评估报告", key: "report" },
|
||||
]}
|
||||
></Breadcrumb>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
className="flex items-center gap-2"
|
||||
icon={<Download className="w-4 h-4" />}
|
||||
>
|
||||
导出报告
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 基本信息卡片 */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<Card>
|
||||
<div className="p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 bg-blue-100 rounded-lg">
|
||||
<BarChart3 className="w-5 h-5 text-blue-600" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-2xl font-bold text-gray-900">
|
||||
{selectedTask.score || 0}
|
||||
</div>
|
||||
<div className="text-sm text-gray-500">总体评分</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<div className="p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 bg-green-100 rounded-lg">
|
||||
<Target className="w-5 h-5 text-green-600" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-2xl font-bold text-gray-900">
|
||||
{selectedTask.sliceConfig?.sampleCount}
|
||||
</div>
|
||||
<div className="text-sm text-gray-500">评估样本数</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<div className="p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 bg-purple-100 rounded-lg">
|
||||
<Users className="w-5 h-5 text-purple-600" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-2xl font-bold text-gray-900">
|
||||
{selectedTask.evaluationType === "manual" ? "人工" : "模型"}
|
||||
</div>
|
||||
<div className="text-sm text-gray-500">评估方式</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<div className="p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 bg-orange-100 rounded-lg">
|
||||
<Calendar className="w-5 h-5 text-orange-600" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-2xl font-bold text-gray-900">
|
||||
{selectedTask.progress}%
|
||||
</div>
|
||||
<div className="text-sm text-gray-500">完成进度</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 详细信息 */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* 评估结果 */}
|
||||
<Card
|
||||
title={
|
||||
<span className="flex items-center gap-2">
|
||||
<TrendingUp className="w-5 h-5" />
|
||||
评估结果
|
||||
</span>
|
||||
}
|
||||
styles={{ body: { paddingTop: 0 } }}
|
||||
>
|
||||
{/* 维度评分 */}
|
||||
<div className="mt-4">
|
||||
<h4 className="font-medium mb-3">维度评分</h4>
|
||||
<div className="space-y-3">
|
||||
{allDimensions.map((dimension) => {
|
||||
const score = 75 + Math.floor(Math.random() * 20); // 模拟评分
|
||||
return (
|
||||
<div
|
||||
key={dimension.id}
|
||||
className="flex items-center justify-between"
|
||||
>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className="text-sm font-medium">
|
||||
{dimension.name}
|
||||
</span>
|
||||
<span className="text-sm font-bold text-blue-600">
|
||||
{score}分
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-full bg-gray-200 rounded-full h-2">
|
||||
<div
|
||||
className="bg-blue-500 h-2 rounded-full"
|
||||
style={{ width: `${score}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 质量分数解读 */}
|
||||
<div className="border-t pt-4 mt-4">
|
||||
<h4 className="font-medium mb-3">质量分数解读</h4>
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-3 h-3 bg-green-500 rounded-full" />
|
||||
<span>90-100分: 优秀,质量很高</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-3 h-3 bg-blue-500 rounded-full" />
|
||||
<span>80-89分: 良好,质量较好</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-3 h-3 bg-yellow-500 rounded-full" />
|
||||
<span>70-79分: 一般,需要改进</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-3 h-3 bg-red-500 rounded-full" />
|
||||
<span>60-69分: 较差,需要重点关注</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* 切片信息 */}
|
||||
<Card
|
||||
title={
|
||||
<span className="flex items-center gap-2">
|
||||
<Scissors className="w-5 h-5" />
|
||||
切片信息
|
||||
</span>
|
||||
}
|
||||
styles={{ body: { paddingTop: 0 } }}
|
||||
>
|
||||
<div className="space-y-4 mt-4">
|
||||
<div className="grid grid-cols-2 gap-4 text-sm">
|
||||
<div>
|
||||
<span className="text-gray-500">切片阈值:</span>
|
||||
<span className="ml-2 font-medium">
|
||||
{selectedTask.sliceConfig?.threshold}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-500">抽样数量:</span>
|
||||
<span className="ml-2 font-medium">
|
||||
{selectedTask.sliceConfig?.sampleCount}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-500">切片方法:</span>
|
||||
<span className="ml-2 font-medium">
|
||||
{selectedTask.sliceConfig?.method}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-500">评估时间:</span>
|
||||
<span className="ml-2 font-medium">
|
||||
{selectedTask.completedAt || selectedTask.createdAt}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t pt-4">
|
||||
<h4 className="font-medium mb-3">评估维度</h4>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{allDimensions.map((dimension) => (
|
||||
<Badge
|
||||
key={dimension.id}
|
||||
style={{
|
||||
border: "1px solid #d9d9d9",
|
||||
background: "#fafafa",
|
||||
padding: "0 8px",
|
||||
}}
|
||||
>
|
||||
{dimension.name}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* QA对详情 */}
|
||||
<Card
|
||||
title={
|
||||
<span className="flex items-center gap-2">
|
||||
<MessageSquare className="w-5 h-5" />
|
||||
QA对详情
|
||||
</span>
|
||||
}
|
||||
styles={{ body: { paddingTop: 0 } }}
|
||||
>
|
||||
<div className="space-y-4 mt-4">
|
||||
{mockQAPairs.map((qa) => (
|
||||
<div key={qa.id} className="border rounded-lg p-4">
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<span className="text-sm font-medium text-gray-700 mb-1">
|
||||
问题:
|
||||
</span>
|
||||
<span className="text-gray-900">{qa.question}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-sm font-medium text-gray-700 mb-1">
|
||||
回答:
|
||||
</span>
|
||||
<span className="text-gray-900">{qa.answer}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between pt-2 border-t">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-gray-500">评分:</span>
|
||||
<div className="flex items-center gap-1">
|
||||
{[1, 2, 3, 4, 5].map((star) => (
|
||||
<Star
|
||||
key={star}
|
||||
className={`w-4 h-4 ${star <= qa.score
|
||||
? "text-yellow-400"
|
||||
: "text-gray-300"
|
||||
}`}
|
||||
style={star <= qa.score ? { fill: "#facc15" } : {}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<span className="text-sm font-medium">{qa.score}/5</span>
|
||||
</div>
|
||||
{qa.feedback && (
|
||||
<div className="text-sm text-gray-600">{qa.feedback}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EvaluationTaskReport;
|
||||
|
||||
@@ -1,276 +1,276 @@
|
||||
import { get, post, put, del, download } from "@/utils/request";
|
||||
|
||||
export function createEvaluationTaskUsingPost(data: any) {
|
||||
return post("/api/evaluation/tasks", data);
|
||||
}
|
||||
|
||||
export function getPagedEvaluationTaskUsingGet(params?: any) {
|
||||
return get("/api/evaluation/tasks", params);
|
||||
}
|
||||
|
||||
export function deleteEvaluationTaskUsingGet(id: string) {
|
||||
const url = `/api/evaluation/tasks?ids=${id}`;
|
||||
return del(url);
|
||||
}
|
||||
|
||||
export function queryPromptTemplatesUsingGet() {
|
||||
return get("/api/evaluation/prompt-templates");
|
||||
}
|
||||
|
||||
export function getEvaluationTaskByIdUsingGet(taskId: string | number) {
|
||||
return get(`/api/evaluation/tasks/${taskId}`);
|
||||
}
|
||||
|
||||
export function queryEvaluationFilesUsingGet(params: {
|
||||
taskId: string;
|
||||
page?: number;
|
||||
size?: number;
|
||||
}) {
|
||||
const { taskId, ...rest } = params;
|
||||
return get(`/api/evaluation/tasks/${taskId}/files`, rest);
|
||||
}
|
||||
|
||||
export function queryEvaluationItemsUsingGet(params: {
|
||||
taskId: string;
|
||||
page?: number;
|
||||
size?: number;
|
||||
status?: string;
|
||||
file_id?: string;
|
||||
}) {
|
||||
const { taskId, ...rest } = params;
|
||||
return get(`/api/evaluation/tasks/${taskId}/items`, rest);
|
||||
}
|
||||
|
||||
// 数据质量评估相关接口
|
||||
export function evaluateDataQualityUsingPost(data: any) {
|
||||
return post("/api/v1/evaluation/quality", data);
|
||||
}
|
||||
|
||||
export function getQualityEvaluationByIdUsingGet(evaluationId: string | number) {
|
||||
return get(`/api/v1/evaluation/quality/${evaluationId}`);
|
||||
}
|
||||
|
||||
// 适配性评估相关接口
|
||||
export function evaluateCompatibilityUsingPost(data: any) {
|
||||
return post("/api/v1/evaluation/compatibility", data);
|
||||
}
|
||||
|
||||
// 价值评估相关接口
|
||||
export function evaluateValueUsingPost(data: any) {
|
||||
return post("/api/v1/evaluation/value", data);
|
||||
}
|
||||
|
||||
// 评估报告管理接口
|
||||
export function queryEvaluationReportsUsingGet(params?: any) {
|
||||
return get("/api/v1/evaluation/reports", params);
|
||||
}
|
||||
|
||||
export function getEvaluationReportByIdUsingGet(reportId: string | number) {
|
||||
return get(`/api/v1/evaluation/reports/${reportId}`);
|
||||
}
|
||||
|
||||
export function exportEvaluationReportUsingGet(reportId: string | number, format = "PDF", filename?: string) {
|
||||
return download(`/api/v1/evaluation/reports/${reportId}/export`, { format }, filename);
|
||||
}
|
||||
|
||||
// 批量评估接口
|
||||
export function batchEvaluationUsingPost(data: any) {
|
||||
return post("/api/v1/evaluation/batch", data);
|
||||
}
|
||||
|
||||
// 扩展功能接口(基于常见需求添加)
|
||||
|
||||
// 评估模板管理
|
||||
export function queryEvaluationTemplatesUsingGet(params?: any) {
|
||||
return get("/api/v1/evaluation/templates", params);
|
||||
}
|
||||
|
||||
export function createEvaluationTemplateUsingPost(data: any) {
|
||||
return post("/api/v1/evaluation/templates", data);
|
||||
}
|
||||
|
||||
export function getEvaluationTemplateByIdUsingGet(templateId: string | number) {
|
||||
return get(`/api/v1/evaluation/templates/${templateId}`);
|
||||
}
|
||||
|
||||
export function updateEvaluationTemplateByIdUsingPut(templateId: string | number, data: any) {
|
||||
return put(`/api/v1/evaluation/templates/${templateId}`, data);
|
||||
}
|
||||
|
||||
export function deleteEvaluationTemplateByIdUsingDelete(templateId: string | number) {
|
||||
return del(`/api/v1/evaluation/templates/${templateId}`);
|
||||
}
|
||||
|
||||
// 评估历史记录
|
||||
export function queryEvaluationHistoryUsingGet(params?: any) {
|
||||
return get("/api/v1/evaluation/history", params);
|
||||
}
|
||||
|
||||
export function getEvaluationHistoryByDatasetUsingGet(datasetId: string | number, params?: any) {
|
||||
return get(`/api/v1/evaluation/history/dataset/${datasetId}`, params);
|
||||
}
|
||||
|
||||
// 评估指标配置
|
||||
export function queryQualityMetricsUsingGet() {
|
||||
return get("/api/v1/evaluation/metrics/quality");
|
||||
}
|
||||
|
||||
export function queryCompatibilityMetricsUsingGet() {
|
||||
return get("/api/v1/evaluation/metrics/compatibility");
|
||||
}
|
||||
|
||||
export function queryValueMetricsUsingGet() {
|
||||
return get("/api/v1/evaluation/metrics/value");
|
||||
}
|
||||
|
||||
// 评估规则管理
|
||||
export function queryEvaluationRulesUsingGet(params?: any) {
|
||||
return get("/api/v1/evaluation/rules", params);
|
||||
}
|
||||
|
||||
export function createEvaluationRuleUsingPost(data: any) {
|
||||
return post("/api/v1/evaluation/rules", data);
|
||||
}
|
||||
|
||||
export function updateEvaluationRuleByIdUsingPut(ruleId: string | number, data: any) {
|
||||
return put(`/api/v1/evaluation/rules/${ruleId}`, data);
|
||||
}
|
||||
|
||||
export function deleteEvaluationRuleByIdUsingDelete(ruleId: string | number) {
|
||||
return del(`/api/v1/evaluation/rules/${ruleId}`);
|
||||
}
|
||||
|
||||
// 评估统计信息
|
||||
export function getEvaluationStatisticsUsingGet(params?: any) {
|
||||
return get("/api/v1/evaluation/statistics", params);
|
||||
}
|
||||
|
||||
export function getDatasetEvaluationSummaryUsingGet(datasetId: string | number) {
|
||||
return get(`/api/v1/evaluation/datasets/${datasetId}/summary`);
|
||||
}
|
||||
|
||||
// 评估任务管理
|
||||
export function queryEvaluationTasksUsingGet(params?: any) {
|
||||
return get("/api/v1/evaluation/tasks", params);
|
||||
}
|
||||
|
||||
export function cancelEvaluationTaskUsingPost(taskId: string | number) {
|
||||
return post(`/api/v1/evaluation/tasks/${taskId}/cancel`);
|
||||
}
|
||||
|
||||
export function retryEvaluationTaskUsingPost(taskId: string | number) {
|
||||
return post(`/api/v1/evaluation/tasks/${taskId}/retry`);
|
||||
}
|
||||
|
||||
// 评估结果比较
|
||||
export function compareEvaluationResultsUsingPost(data: any) {
|
||||
return post("/api/v1/evaluation/compare", data);
|
||||
}
|
||||
|
||||
// 评估配置管理
|
||||
export function getEvaluationConfigUsingGet() {
|
||||
return get("/api/v1/evaluation/config");
|
||||
}
|
||||
|
||||
export function updateEvaluationConfigUsingPut(data: any) {
|
||||
return put("/api/v1/evaluation/config", data);
|
||||
}
|
||||
|
||||
// 数据质量监控
|
||||
export function createQualityMonitorUsingPost(data: any) {
|
||||
return post("/api/v1/evaluation/quality/monitors", data);
|
||||
}
|
||||
|
||||
export function queryQualityMonitorsUsingGet(params?: any) {
|
||||
return get("/api/v1/evaluation/quality/monitors", params);
|
||||
}
|
||||
|
||||
export function updateQualityMonitorByIdUsingPut(monitorId: string | number, data: any) {
|
||||
return put(`/api/v1/evaluation/quality/monitors/${monitorId}`, data);
|
||||
}
|
||||
|
||||
export function deleteQualityMonitorByIdUsingDelete(monitorId: string | number) {
|
||||
return del(`/api/v1/evaluation/quality/monitors/${monitorId}`);
|
||||
}
|
||||
|
||||
// 评估基准管理
|
||||
export function queryEvaluationBenchmarksUsingGet(params?: any) {
|
||||
return get("/api/v1/evaluation/benchmarks", params);
|
||||
}
|
||||
|
||||
export function createEvaluationBenchmarkUsingPost(data: any) {
|
||||
return post("/api/v1/evaluation/benchmarks", data);
|
||||
}
|
||||
|
||||
export function updateEvaluationBenchmarkByIdUsingPut(benchmarkId: string | number, data: any) {
|
||||
return put(`/api/v1/evaluation/benchmarks/${benchmarkId}`, data);
|
||||
}
|
||||
|
||||
export function deleteEvaluationBenchmarkByIdUsingDelete(benchmarkId: string | number) {
|
||||
return del(`/api/v1/evaluation/benchmarks/${benchmarkId}`);
|
||||
}
|
||||
|
||||
// 评估算法管理
|
||||
export function queryEvaluationAlgorithmsUsingGet(params?: any) {
|
||||
return get("/api/v1/evaluation/algorithms", params);
|
||||
}
|
||||
|
||||
export function runCustomEvaluationUsingPost(data: any) {
|
||||
return post("/api/v1/evaluation/custom", data);
|
||||
}
|
||||
|
||||
// 评估可视化数据
|
||||
export function getEvaluationVisualizationUsingGet(evaluationId: string | number, chartType?: string) {
|
||||
return get(`/api/v1/evaluation/${evaluationId}/visualization`, { chartType });
|
||||
}
|
||||
|
||||
// 评估通知和警报
|
||||
export function queryEvaluationAlertsUsingGet(params?: any) {
|
||||
return get("/api/v1/evaluation/alerts", params);
|
||||
}
|
||||
|
||||
export function createEvaluationAlertUsingPost(data: any) {
|
||||
return post("/api/v1/evaluation/alerts", data);
|
||||
}
|
||||
|
||||
export function updateEvaluationAlertByIdUsingPut(alertId: string | number, data: any) {
|
||||
return put(`/api/v1/evaluation/alerts/${alertId}`, data);
|
||||
}
|
||||
|
||||
export function deleteEvaluationAlertByIdUsingDelete(alertId: string | number) {
|
||||
return del(`/api/v1/evaluation/alerts/${alertId}`);
|
||||
}
|
||||
|
||||
// 批量操作扩展
|
||||
export function batchDeleteEvaluationReportsUsingPost(data: { reportIds: string[] }) {
|
||||
return post("/api/v1/evaluation/reports/batch-delete", data);
|
||||
}
|
||||
|
||||
export function batchExportEvaluationReportsUsingPost(data: any) {
|
||||
return post("/api/v1/evaluation/reports/batch-export", data);
|
||||
}
|
||||
|
||||
// 评估调度管理
|
||||
export function queryEvaluationSchedulesUsingGet(params?: any) {
|
||||
return get("/api/v1/evaluation/schedules", params);
|
||||
}
|
||||
|
||||
export function createEvaluationScheduleUsingPost(data: any) {
|
||||
return post("/api/v1/evaluation/schedules", data);
|
||||
}
|
||||
|
||||
export function updateEvaluationScheduleByIdUsingPut(scheduleId: string | number, data: any) {
|
||||
return put(`/api/v1/evaluation/schedules/${scheduleId}`, data);
|
||||
}
|
||||
|
||||
export function deleteEvaluationScheduleByIdUsingDelete(scheduleId: string | number) {
|
||||
return del(`/api/v1/evaluation/schedules/${scheduleId}`);
|
||||
}
|
||||
|
||||
export function enableEvaluationScheduleUsingPost(scheduleId: string | number) {
|
||||
return post(`/api/v1/evaluation/schedules/${scheduleId}/enable`);
|
||||
}
|
||||
|
||||
export function disableEvaluationScheduleUsingPost(scheduleId: string | number) {
|
||||
return post(`/api/v1/evaluation/schedules/${scheduleId}/disable`);
|
||||
}
|
||||
import { get, post, put, del, download } from "@/utils/request";
|
||||
|
||||
export function createEvaluationTaskUsingPost(data: any) {
|
||||
return post("/api/evaluation/tasks", data);
|
||||
}
|
||||
|
||||
export function getPagedEvaluationTaskUsingGet(params?: any) {
|
||||
return get("/api/evaluation/tasks", params);
|
||||
}
|
||||
|
||||
export function deleteEvaluationTaskUsingGet(id: string) {
|
||||
const url = `/api/evaluation/tasks?ids=${id}`;
|
||||
return del(url);
|
||||
}
|
||||
|
||||
export function queryPromptTemplatesUsingGet() {
|
||||
return get("/api/evaluation/prompt-templates");
|
||||
}
|
||||
|
||||
export function getEvaluationTaskByIdUsingGet(taskId: string | number) {
|
||||
return get(`/api/evaluation/tasks/${taskId}`);
|
||||
}
|
||||
|
||||
export function queryEvaluationFilesUsingGet(params: {
|
||||
taskId: string;
|
||||
page?: number;
|
||||
size?: number;
|
||||
}) {
|
||||
const { taskId, ...rest } = params;
|
||||
return get(`/api/evaluation/tasks/${taskId}/files`, rest);
|
||||
}
|
||||
|
||||
export function queryEvaluationItemsUsingGet(params: {
|
||||
taskId: string;
|
||||
page?: number;
|
||||
size?: number;
|
||||
status?: string;
|
||||
file_id?: string;
|
||||
}) {
|
||||
const { taskId, ...rest } = params;
|
||||
return get(`/api/evaluation/tasks/${taskId}/items`, rest);
|
||||
}
|
||||
|
||||
// 数据质量评估相关接口
|
||||
export function evaluateDataQualityUsingPost(data: any) {
|
||||
return post("/api/v1/evaluation/quality", data);
|
||||
}
|
||||
|
||||
export function getQualityEvaluationByIdUsingGet(evaluationId: string | number) {
|
||||
return get(`/api/v1/evaluation/quality/${evaluationId}`);
|
||||
}
|
||||
|
||||
// 适配性评估相关接口
|
||||
export function evaluateCompatibilityUsingPost(data: any) {
|
||||
return post("/api/v1/evaluation/compatibility", data);
|
||||
}
|
||||
|
||||
// 价值评估相关接口
|
||||
export function evaluateValueUsingPost(data: any) {
|
||||
return post("/api/v1/evaluation/value", data);
|
||||
}
|
||||
|
||||
// 评估报告管理接口
|
||||
export function queryEvaluationReportsUsingGet(params?: any) {
|
||||
return get("/api/v1/evaluation/reports", params);
|
||||
}
|
||||
|
||||
export function getEvaluationReportByIdUsingGet(reportId: string | number) {
|
||||
return get(`/api/v1/evaluation/reports/${reportId}`);
|
||||
}
|
||||
|
||||
export function exportEvaluationReportUsingGet(reportId: string | number, format = "PDF", filename?: string) {
|
||||
return download(`/api/v1/evaluation/reports/${reportId}/export`, { format }, filename);
|
||||
}
|
||||
|
||||
// 批量评估接口
|
||||
export function batchEvaluationUsingPost(data: any) {
|
||||
return post("/api/v1/evaluation/batch", data);
|
||||
}
|
||||
|
||||
// 扩展功能接口(基于常见需求添加)
|
||||
|
||||
// 评估模板管理
|
||||
export function queryEvaluationTemplatesUsingGet(params?: any) {
|
||||
return get("/api/v1/evaluation/templates", params);
|
||||
}
|
||||
|
||||
export function createEvaluationTemplateUsingPost(data: any) {
|
||||
return post("/api/v1/evaluation/templates", data);
|
||||
}
|
||||
|
||||
export function getEvaluationTemplateByIdUsingGet(templateId: string | number) {
|
||||
return get(`/api/v1/evaluation/templates/${templateId}`);
|
||||
}
|
||||
|
||||
export function updateEvaluationTemplateByIdUsingPut(templateId: string | number, data: any) {
|
||||
return put(`/api/v1/evaluation/templates/${templateId}`, data);
|
||||
}
|
||||
|
||||
export function deleteEvaluationTemplateByIdUsingDelete(templateId: string | number) {
|
||||
return del(`/api/v1/evaluation/templates/${templateId}`);
|
||||
}
|
||||
|
||||
// 评估历史记录
|
||||
export function queryEvaluationHistoryUsingGet(params?: any) {
|
||||
return get("/api/v1/evaluation/history", params);
|
||||
}
|
||||
|
||||
export function getEvaluationHistoryByDatasetUsingGet(datasetId: string | number, params?: any) {
|
||||
return get(`/api/v1/evaluation/history/dataset/${datasetId}`, params);
|
||||
}
|
||||
|
||||
// 评估指标配置
|
||||
export function queryQualityMetricsUsingGet() {
|
||||
return get("/api/v1/evaluation/metrics/quality");
|
||||
}
|
||||
|
||||
export function queryCompatibilityMetricsUsingGet() {
|
||||
return get("/api/v1/evaluation/metrics/compatibility");
|
||||
}
|
||||
|
||||
export function queryValueMetricsUsingGet() {
|
||||
return get("/api/v1/evaluation/metrics/value");
|
||||
}
|
||||
|
||||
// 评估规则管理
|
||||
export function queryEvaluationRulesUsingGet(params?: any) {
|
||||
return get("/api/v1/evaluation/rules", params);
|
||||
}
|
||||
|
||||
export function createEvaluationRuleUsingPost(data: any) {
|
||||
return post("/api/v1/evaluation/rules", data);
|
||||
}
|
||||
|
||||
export function updateEvaluationRuleByIdUsingPut(ruleId: string | number, data: any) {
|
||||
return put(`/api/v1/evaluation/rules/${ruleId}`, data);
|
||||
}
|
||||
|
||||
export function deleteEvaluationRuleByIdUsingDelete(ruleId: string | number) {
|
||||
return del(`/api/v1/evaluation/rules/${ruleId}`);
|
||||
}
|
||||
|
||||
// 评估统计信息
|
||||
export function getEvaluationStatisticsUsingGet(params?: any) {
|
||||
return get("/api/v1/evaluation/statistics", params);
|
||||
}
|
||||
|
||||
export function getDatasetEvaluationSummaryUsingGet(datasetId: string | number) {
|
||||
return get(`/api/v1/evaluation/datasets/${datasetId}/summary`);
|
||||
}
|
||||
|
||||
// 评估任务管理
|
||||
export function queryEvaluationTasksUsingGet(params?: any) {
|
||||
return get("/api/v1/evaluation/tasks", params);
|
||||
}
|
||||
|
||||
export function cancelEvaluationTaskUsingPost(taskId: string | number) {
|
||||
return post(`/api/v1/evaluation/tasks/${taskId}/cancel`);
|
||||
}
|
||||
|
||||
export function retryEvaluationTaskUsingPost(taskId: string | number) {
|
||||
return post(`/api/v1/evaluation/tasks/${taskId}/retry`);
|
||||
}
|
||||
|
||||
// 评估结果比较
|
||||
export function compareEvaluationResultsUsingPost(data: any) {
|
||||
return post("/api/v1/evaluation/compare", data);
|
||||
}
|
||||
|
||||
// 评估配置管理
|
||||
export function getEvaluationConfigUsingGet() {
|
||||
return get("/api/v1/evaluation/config");
|
||||
}
|
||||
|
||||
export function updateEvaluationConfigUsingPut(data: any) {
|
||||
return put("/api/v1/evaluation/config", data);
|
||||
}
|
||||
|
||||
// 数据质量监控
|
||||
export function createQualityMonitorUsingPost(data: any) {
|
||||
return post("/api/v1/evaluation/quality/monitors", data);
|
||||
}
|
||||
|
||||
export function queryQualityMonitorsUsingGet(params?: any) {
|
||||
return get("/api/v1/evaluation/quality/monitors", params);
|
||||
}
|
||||
|
||||
export function updateQualityMonitorByIdUsingPut(monitorId: string | number, data: any) {
|
||||
return put(`/api/v1/evaluation/quality/monitors/${monitorId}`, data);
|
||||
}
|
||||
|
||||
export function deleteQualityMonitorByIdUsingDelete(monitorId: string | number) {
|
||||
return del(`/api/v1/evaluation/quality/monitors/${monitorId}`);
|
||||
}
|
||||
|
||||
// 评估基准管理
|
||||
export function queryEvaluationBenchmarksUsingGet(params?: any) {
|
||||
return get("/api/v1/evaluation/benchmarks", params);
|
||||
}
|
||||
|
||||
export function createEvaluationBenchmarkUsingPost(data: any) {
|
||||
return post("/api/v1/evaluation/benchmarks", data);
|
||||
}
|
||||
|
||||
export function updateEvaluationBenchmarkByIdUsingPut(benchmarkId: string | number, data: any) {
|
||||
return put(`/api/v1/evaluation/benchmarks/${benchmarkId}`, data);
|
||||
}
|
||||
|
||||
export function deleteEvaluationBenchmarkByIdUsingDelete(benchmarkId: string | number) {
|
||||
return del(`/api/v1/evaluation/benchmarks/${benchmarkId}`);
|
||||
}
|
||||
|
||||
// 评估算法管理
|
||||
export function queryEvaluationAlgorithmsUsingGet(params?: any) {
|
||||
return get("/api/v1/evaluation/algorithms", params);
|
||||
}
|
||||
|
||||
export function runCustomEvaluationUsingPost(data: any) {
|
||||
return post("/api/v1/evaluation/custom", data);
|
||||
}
|
||||
|
||||
// 评估可视化数据
|
||||
export function getEvaluationVisualizationUsingGet(evaluationId: string | number, chartType?: string) {
|
||||
return get(`/api/v1/evaluation/${evaluationId}/visualization`, { chartType });
|
||||
}
|
||||
|
||||
// 评估通知和警报
|
||||
export function queryEvaluationAlertsUsingGet(params?: any) {
|
||||
return get("/api/v1/evaluation/alerts", params);
|
||||
}
|
||||
|
||||
export function createEvaluationAlertUsingPost(data: any) {
|
||||
return post("/api/v1/evaluation/alerts", data);
|
||||
}
|
||||
|
||||
export function updateEvaluationAlertByIdUsingPut(alertId: string | number, data: any) {
|
||||
return put(`/api/v1/evaluation/alerts/${alertId}`, data);
|
||||
}
|
||||
|
||||
export function deleteEvaluationAlertByIdUsingDelete(alertId: string | number) {
|
||||
return del(`/api/v1/evaluation/alerts/${alertId}`);
|
||||
}
|
||||
|
||||
// 批量操作扩展
|
||||
export function batchDeleteEvaluationReportsUsingPost(data: { reportIds: string[] }) {
|
||||
return post("/api/v1/evaluation/reports/batch-delete", data);
|
||||
}
|
||||
|
||||
export function batchExportEvaluationReportsUsingPost(data: any) {
|
||||
return post("/api/v1/evaluation/reports/batch-export", data);
|
||||
}
|
||||
|
||||
// 评估调度管理
|
||||
export function queryEvaluationSchedulesUsingGet(params?: any) {
|
||||
return get("/api/v1/evaluation/schedules", params);
|
||||
}
|
||||
|
||||
export function createEvaluationScheduleUsingPost(data: any) {
|
||||
return post("/api/v1/evaluation/schedules", data);
|
||||
}
|
||||
|
||||
export function updateEvaluationScheduleByIdUsingPut(scheduleId: string | number, data: any) {
|
||||
return put(`/api/v1/evaluation/schedules/${scheduleId}`, data);
|
||||
}
|
||||
|
||||
export function deleteEvaluationScheduleByIdUsingDelete(scheduleId: string | number) {
|
||||
return del(`/api/v1/evaluation/schedules/${scheduleId}`);
|
||||
}
|
||||
|
||||
export function enableEvaluationScheduleUsingPost(scheduleId: string | number) {
|
||||
return post(`/api/v1/evaluation/schedules/${scheduleId}/enable`);
|
||||
}
|
||||
|
||||
export function disableEvaluationScheduleUsingPost(scheduleId: string | number) {
|
||||
return post(`/api/v1/evaluation/schedules/${scheduleId}/disable`);
|
||||
}
|
||||
|
||||
@@ -1,95 +1,95 @@
|
||||
import { formatDateTime } from "@/utils/unit";
|
||||
import { BarChart3 } from "lucide-react";
|
||||
import { EvaluationStatus, EvaluationTask } from "@/pages/DataEvaluation/evaluation.model.ts";
|
||||
|
||||
export const TASK_TYPES = [
|
||||
{ label: 'QA评估', value: 'QA' },
|
||||
{ label: 'COT评估', value: 'COT' },
|
||||
];
|
||||
|
||||
export const EVAL_METHODS = [
|
||||
{ label: '模型自动评估', value: 'AUTO' },
|
||||
];
|
||||
|
||||
export const getEvalType = (type: string) => {
|
||||
return TASK_TYPES.find((item) => item.value === type)?.label;
|
||||
};
|
||||
|
||||
export const getEvalMethod = (type: string) => {
|
||||
return EVAL_METHODS.find((item) => item.value === type)?.label;
|
||||
};
|
||||
|
||||
export const getSource = (type: string) => {
|
||||
switch (type) {
|
||||
case "DATASET":
|
||||
return "数据集 - ";
|
||||
case "SYNTHESIS":
|
||||
return "合成任务 - ";
|
||||
default:
|
||||
return "-";
|
||||
}
|
||||
};
|
||||
|
||||
export const evalTaskStatusMap: Record<
|
||||
string,
|
||||
{
|
||||
value: EvaluationStatus;
|
||||
label: string;
|
||||
color: string;
|
||||
}
|
||||
> = {
|
||||
[EvaluationStatus.PENDING]: {
|
||||
value: EvaluationStatus.PENDING,
|
||||
label: "等待中",
|
||||
color: "gray",
|
||||
},
|
||||
[EvaluationStatus.RUNNING]: {
|
||||
value: EvaluationStatus.RUNNING,
|
||||
label: "运行中",
|
||||
color: "blue",
|
||||
},
|
||||
[EvaluationStatus.COMPLETED]: {
|
||||
value: EvaluationStatus.COMPLETED,
|
||||
label: "已完成",
|
||||
color: "green",
|
||||
},
|
||||
[EvaluationStatus.FAILED]: {
|
||||
value: EvaluationStatus.FAILED,
|
||||
label: "失败",
|
||||
color: "red",
|
||||
},
|
||||
[EvaluationStatus.PAUSED]: {
|
||||
value: EvaluationStatus.PAUSED,
|
||||
label: "已暂停",
|
||||
color: "orange",
|
||||
},
|
||||
};
|
||||
|
||||
export function mapEvaluationTask(task: Partial<EvaluationTask>): EvaluationTask {
|
||||
return {
|
||||
...task,
|
||||
status: evalTaskStatusMap[task.status || EvaluationStatus.PENDING],
|
||||
createdAt: formatDateTime(task.createdAt),
|
||||
updatedAt: formatDateTime(task.updatedAt),
|
||||
description: task.description,
|
||||
icon: <BarChart3 />,
|
||||
iconColor: task.ratio_method === "DATASET" ? "bg-blue-100" : "bg-green-100",
|
||||
statistics: [
|
||||
{
|
||||
label: "任务类型",
|
||||
icon: <BarChart3 className="w-4 h-4 text-gray-500" />,
|
||||
value: (task.taskType ?? 0).toLocaleString(),
|
||||
},
|
||||
{
|
||||
label: "评估方式",
|
||||
icon: <BarChart3 className="w-4 h-4 text-gray-500" />,
|
||||
value: (task.evalMethod ?? 0).toLocaleString(),
|
||||
},
|
||||
{
|
||||
label: "数据源",
|
||||
icon: <BarChart3 className="w-4 h-4 text-gray-500" />,
|
||||
value: (task.sourceName ?? 0).toLocaleString(),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
import { formatDateTime } from "@/utils/unit";
|
||||
import { BarChart3 } from "lucide-react";
|
||||
import { EvaluationStatus, EvaluationTask } from "@/pages/DataEvaluation/evaluation.model.ts";
|
||||
|
||||
export const TASK_TYPES = [
|
||||
{ label: 'QA评估', value: 'QA' },
|
||||
{ label: 'COT评估', value: 'COT' },
|
||||
];
|
||||
|
||||
export const EVAL_METHODS = [
|
||||
{ label: '模型自动评估', value: 'AUTO' },
|
||||
];
|
||||
|
||||
export const getEvalType = (type: string) => {
|
||||
return TASK_TYPES.find((item) => item.value === type)?.label;
|
||||
};
|
||||
|
||||
export const getEvalMethod = (type: string) => {
|
||||
return EVAL_METHODS.find((item) => item.value === type)?.label;
|
||||
};
|
||||
|
||||
export const getSource = (type: string) => {
|
||||
switch (type) {
|
||||
case "DATASET":
|
||||
return "数据集 - ";
|
||||
case "SYNTHESIS":
|
||||
return "合成任务 - ";
|
||||
default:
|
||||
return "-";
|
||||
}
|
||||
};
|
||||
|
||||
export const evalTaskStatusMap: Record<
|
||||
string,
|
||||
{
|
||||
value: EvaluationStatus;
|
||||
label: string;
|
||||
color: string;
|
||||
}
|
||||
> = {
|
||||
[EvaluationStatus.PENDING]: {
|
||||
value: EvaluationStatus.PENDING,
|
||||
label: "等待中",
|
||||
color: "gray",
|
||||
},
|
||||
[EvaluationStatus.RUNNING]: {
|
||||
value: EvaluationStatus.RUNNING,
|
||||
label: "运行中",
|
||||
color: "blue",
|
||||
},
|
||||
[EvaluationStatus.COMPLETED]: {
|
||||
value: EvaluationStatus.COMPLETED,
|
||||
label: "已完成",
|
||||
color: "green",
|
||||
},
|
||||
[EvaluationStatus.FAILED]: {
|
||||
value: EvaluationStatus.FAILED,
|
||||
label: "失败",
|
||||
color: "red",
|
||||
},
|
||||
[EvaluationStatus.PAUSED]: {
|
||||
value: EvaluationStatus.PAUSED,
|
||||
label: "已暂停",
|
||||
color: "orange",
|
||||
},
|
||||
};
|
||||
|
||||
export function mapEvaluationTask(task: Partial<EvaluationTask>): EvaluationTask {
|
||||
return {
|
||||
...task,
|
||||
status: evalTaskStatusMap[task.status || EvaluationStatus.PENDING],
|
||||
createdAt: formatDateTime(task.createdAt),
|
||||
updatedAt: formatDateTime(task.updatedAt),
|
||||
description: task.description,
|
||||
icon: <BarChart3 />,
|
||||
iconColor: task.ratio_method === "DATASET" ? "bg-blue-100" : "bg-green-100",
|
||||
statistics: [
|
||||
{
|
||||
label: "任务类型",
|
||||
icon: <BarChart3 className="w-4 h-4 text-gray-500" />,
|
||||
value: (task.taskType ?? 0).toLocaleString(),
|
||||
},
|
||||
{
|
||||
label: "评估方式",
|
||||
icon: <BarChart3 className="w-4 h-4 text-gray-500" />,
|
||||
value: (task.evalMethod ?? 0).toLocaleString(),
|
||||
},
|
||||
{
|
||||
label: "数据源",
|
||||
icon: <BarChart3 className="w-4 h-4 text-gray-500" />,
|
||||
value: (task.sourceName ?? 0).toLocaleString(),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,57 +1,57 @@
|
||||
export enum EvaluationStatus {
|
||||
PENDING = "PENDING",
|
||||
RUNNING = "RUNNING",
|
||||
COMPLETED = "COMPLETED",
|
||||
FAILED = "FAILED",
|
||||
PAUSED = "PAUSED",
|
||||
}
|
||||
|
||||
export interface EvaluationTask {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
taskType: string;
|
||||
sourceType: string;
|
||||
sourceId: string;
|
||||
sourceName: string;
|
||||
status: 'PENDING' | 'RUNNING' | 'COMPLETED' | 'STOPPED' | 'FAILED';
|
||||
evalProcess: number;
|
||||
evalMethod: 'AUTO' | 'MANUAL';
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
interface EvaluationDimension {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
category: "quality" | "accuracy" | "completeness" | "consistency" | "bias" | "custom"
|
||||
isCustom?: boolean
|
||||
isEnabled?: boolean
|
||||
}
|
||||
|
||||
interface EvaluationSlice {
|
||||
id: string
|
||||
content: string
|
||||
sourceFile: string
|
||||
sliceIndex: number
|
||||
sliceType: string
|
||||
metadata: {
|
||||
startPosition?: number
|
||||
endPosition?: number
|
||||
pageNumber?: number
|
||||
section?: string
|
||||
processingMethod: string
|
||||
}
|
||||
scores?: { [dimensionId: string]: number }
|
||||
comment?: string
|
||||
}
|
||||
|
||||
interface QAPair {
|
||||
id: string
|
||||
question: string
|
||||
answer: string
|
||||
sliceId: string
|
||||
score: number
|
||||
feedback?: string
|
||||
}
|
||||
export enum EvaluationStatus {
|
||||
PENDING = "PENDING",
|
||||
RUNNING = "RUNNING",
|
||||
COMPLETED = "COMPLETED",
|
||||
FAILED = "FAILED",
|
||||
PAUSED = "PAUSED",
|
||||
}
|
||||
|
||||
export interface EvaluationTask {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
taskType: string;
|
||||
sourceType: string;
|
||||
sourceId: string;
|
||||
sourceName: string;
|
||||
status: 'PENDING' | 'RUNNING' | 'COMPLETED' | 'STOPPED' | 'FAILED';
|
||||
evalProcess: number;
|
||||
evalMethod: 'AUTO' | 'MANUAL';
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
interface EvaluationDimension {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
category: "quality" | "accuracy" | "completeness" | "consistency" | "bias" | "custom"
|
||||
isCustom?: boolean
|
||||
isEnabled?: boolean
|
||||
}
|
||||
|
||||
interface EvaluationSlice {
|
||||
id: string
|
||||
content: string
|
||||
sourceFile: string
|
||||
sliceIndex: number
|
||||
sliceType: string
|
||||
metadata: {
|
||||
startPosition?: number
|
||||
endPosition?: number
|
||||
pageNumber?: number
|
||||
section?: string
|
||||
processingMethod: string
|
||||
}
|
||||
scores?: { [dimensionId: string]: number }
|
||||
comment?: string
|
||||
}
|
||||
|
||||
interface QAPair {
|
||||
id: string
|
||||
question: string
|
||||
answer: string
|
||||
sliceId: string
|
||||
score: number
|
||||
feedback?: string
|
||||
}
|
||||
|
||||
@@ -1,83 +1,83 @@
|
||||
import { useState } from "react";
|
||||
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
import { Button, Form, App } from "antd";
|
||||
import { Link, useNavigate } from "react-router";
|
||||
import { createDatasetUsingPost } from "../dataset.api";
|
||||
import { DatasetType } from "../dataset.model";
|
||||
import BasicInformation from "./components/BasicInformation";
|
||||
|
||||
export default function DatasetCreate() {
|
||||
const navigate = useNavigate();
|
||||
const { message } = App.useApp();
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const [newDataset, setNewDataset] = useState({
|
||||
name: "",
|
||||
description: "",
|
||||
datasetType: DatasetType.TEXT,
|
||||
tags: [],
|
||||
});
|
||||
|
||||
const handleSubmit = async () => {
|
||||
const formValues = await form.validateFields();
|
||||
|
||||
const params = {
|
||||
...formValues,
|
||||
files: undefined,
|
||||
};
|
||||
try {
|
||||
const { data } = await createDatasetUsingPost(params);
|
||||
message.success(`数据集创建成功`);
|
||||
navigate("/data/management/detail/" + data.id);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
message.error("数据集创建失败,请重试");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const handleValuesChange = (_, allValues) => {
|
||||
setNewDataset({ ...newDataset, ...allValues });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col flex-1">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center">
|
||||
<Link to="/data/management">
|
||||
<Button type="text">
|
||||
<ArrowLeft className="w-4 h-4 mr-1" />
|
||||
</Button>
|
||||
</Link>
|
||||
<h1 className="text-xl font-bold bg-clip-text">创建数据集</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* form */}
|
||||
<div className="flex-overflow-auto border-card">
|
||||
<div className="flex-1 p-6 overflow-auto">
|
||||
<Form
|
||||
form={form}
|
||||
initialValues={newDataset}
|
||||
onValuesChange={handleValuesChange}
|
||||
layout="vertical"
|
||||
>
|
||||
<BasicInformation data={newDataset} setData={setNewDataset} />
|
||||
</Form>
|
||||
</div>
|
||||
<div className="flex gap-2 justify-end p-6 border-top">
|
||||
<Button onClick={() => navigate("/data/management")}>取消</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
disabled={!newDataset.name || !newDataset.datasetType}
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
确定
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
import { useState } from "react";
|
||||
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
import { Button, Form, App } from "antd";
|
||||
import { Link, useNavigate } from "react-router";
|
||||
import { createDatasetUsingPost } from "../dataset.api";
|
||||
import { DatasetType } from "../dataset.model";
|
||||
import BasicInformation from "./components/BasicInformation";
|
||||
|
||||
export default function DatasetCreate() {
|
||||
const navigate = useNavigate();
|
||||
const { message } = App.useApp();
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const [newDataset, setNewDataset] = useState({
|
||||
name: "",
|
||||
description: "",
|
||||
datasetType: DatasetType.TEXT,
|
||||
tags: [],
|
||||
});
|
||||
|
||||
const handleSubmit = async () => {
|
||||
const formValues = await form.validateFields();
|
||||
|
||||
const params = {
|
||||
...formValues,
|
||||
files: undefined,
|
||||
};
|
||||
try {
|
||||
const { data } = await createDatasetUsingPost(params);
|
||||
message.success(`数据集创建成功`);
|
||||
navigate("/data/management/detail/" + data.id);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
message.error("数据集创建失败,请重试");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const handleValuesChange = (_, allValues) => {
|
||||
setNewDataset({ ...newDataset, ...allValues });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col flex-1">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center">
|
||||
<Link to="/data/management">
|
||||
<Button type="text">
|
||||
<ArrowLeft className="w-4 h-4 mr-1" />
|
||||
</Button>
|
||||
</Link>
|
||||
<h1 className="text-xl font-bold bg-clip-text">创建数据集</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* form */}
|
||||
<div className="flex-overflow-auto border-card">
|
||||
<div className="flex-1 p-6 overflow-auto">
|
||||
<Form
|
||||
form={form}
|
||||
initialValues={newDataset}
|
||||
onValuesChange={handleValuesChange}
|
||||
layout="vertical"
|
||||
>
|
||||
<BasicInformation data={newDataset} setData={setNewDataset} />
|
||||
</Form>
|
||||
</div>
|
||||
<div className="flex gap-2 justify-end p-6 border-top">
|
||||
<Button onClick={() => navigate("/data/management")}>取消</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
disabled={!newDataset.name || !newDataset.datasetType}
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
确定
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,102 +1,102 @@
|
||||
import BasicInformation from "./components/BasicInformation";
|
||||
import {
|
||||
queryDatasetByIdUsingGet,
|
||||
updateDatasetByIdUsingPut,
|
||||
} from "../dataset.api";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Dataset, DatasetType } from "../dataset.model";
|
||||
import { App, Button, Form, Modal } from "antd";
|
||||
|
||||
export default function EditDataset({
|
||||
open,
|
||||
data,
|
||||
onClose,
|
||||
onRefresh,
|
||||
}: {
|
||||
open: boolean;
|
||||
data: Dataset | null;
|
||||
onClose: () => void;
|
||||
onRefresh?: (showMessage?: boolean) => void;
|
||||
}) {
|
||||
const [form] = Form.useForm();
|
||||
const { message } = App.useApp();
|
||||
|
||||
const [newDataset, setNewDataset] = useState({
|
||||
name: "",
|
||||
description: "",
|
||||
datasetType: DatasetType.TEXT,
|
||||
tags: [],
|
||||
});
|
||||
const fetchDataset = async () => {
|
||||
if (!open) return;
|
||||
// 如果有id,说明是编辑模式
|
||||
if (data && data.id) {
|
||||
const { data: newData } = await queryDatasetByIdUsingGet(data.id);
|
||||
const updatedDataset = {
|
||||
...newData,
|
||||
type: newData.type,
|
||||
tags: newData.tags.map((tag) => tag.name) || [],
|
||||
};
|
||||
setNewDataset(updatedDataset);
|
||||
form.setFieldsValue(updatedDataset);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchDataset();
|
||||
}, [data]);
|
||||
|
||||
const handleValuesChange = (_, allValues) => {
|
||||
setNewDataset({ ...newDataset, ...allValues });
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
const formValues = await form.validateFields();
|
||||
|
||||
const params = {
|
||||
...formValues,
|
||||
files: undefined,
|
||||
};
|
||||
try {
|
||||
await updateDatasetByIdUsingPut(data?.id, params);
|
||||
onClose();
|
||||
message.success("数据集更新成功");
|
||||
onRefresh?.(false);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
message.error("数据集更新失败,请重试");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={`编辑数据集${data?.name}`}
|
||||
onCancel={onClose}
|
||||
open={open}
|
||||
width={600}
|
||||
maskClosable={false}
|
||||
footer={
|
||||
<>
|
||||
<Button onClick={onClose}>取消</Button>
|
||||
<Button type="primary" onClick={handleSubmit}>
|
||||
确定
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
initialValues={newDataset}
|
||||
onValuesChange={handleValuesChange}
|
||||
layout="vertical"
|
||||
>
|
||||
<BasicInformation
|
||||
data={newDataset}
|
||||
setData={setNewDataset}
|
||||
hidden={["datasetType"]}
|
||||
/>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
import BasicInformation from "./components/BasicInformation";
|
||||
import {
|
||||
queryDatasetByIdUsingGet,
|
||||
updateDatasetByIdUsingPut,
|
||||
} from "../dataset.api";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Dataset, DatasetType } from "../dataset.model";
|
||||
import { App, Button, Form, Modal } from "antd";
|
||||
|
||||
export default function EditDataset({
|
||||
open,
|
||||
data,
|
||||
onClose,
|
||||
onRefresh,
|
||||
}: {
|
||||
open: boolean;
|
||||
data: Dataset | null;
|
||||
onClose: () => void;
|
||||
onRefresh?: (showMessage?: boolean) => void;
|
||||
}) {
|
||||
const [form] = Form.useForm();
|
||||
const { message } = App.useApp();
|
||||
|
||||
const [newDataset, setNewDataset] = useState({
|
||||
name: "",
|
||||
description: "",
|
||||
datasetType: DatasetType.TEXT,
|
||||
tags: [],
|
||||
});
|
||||
const fetchDataset = async () => {
|
||||
if (!open) return;
|
||||
// 如果有id,说明是编辑模式
|
||||
if (data && data.id) {
|
||||
const { data: newData } = await queryDatasetByIdUsingGet(data.id);
|
||||
const updatedDataset = {
|
||||
...newData,
|
||||
type: newData.type,
|
||||
tags: newData.tags.map((tag) => tag.name) || [],
|
||||
};
|
||||
setNewDataset(updatedDataset);
|
||||
form.setFieldsValue(updatedDataset);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchDataset();
|
||||
}, [data]);
|
||||
|
||||
const handleValuesChange = (_, allValues) => {
|
||||
setNewDataset({ ...newDataset, ...allValues });
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
const formValues = await form.validateFields();
|
||||
|
||||
const params = {
|
||||
...formValues,
|
||||
files: undefined,
|
||||
};
|
||||
try {
|
||||
await updateDatasetByIdUsingPut(data?.id, params);
|
||||
onClose();
|
||||
message.success("数据集更新成功");
|
||||
onRefresh?.(false);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
message.error("数据集更新失败,请重试");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={`编辑数据集${data?.name}`}
|
||||
onCancel={onClose}
|
||||
open={open}
|
||||
width={600}
|
||||
maskClosable={false}
|
||||
footer={
|
||||
<>
|
||||
<Button onClick={onClose}>取消</Button>
|
||||
<Button type="primary" onClick={handleSubmit}>
|
||||
确定
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
initialValues={newDataset}
|
||||
onValuesChange={handleValuesChange}
|
||||
layout="vertical"
|
||||
>
|
||||
<BasicInformation
|
||||
data={newDataset}
|
||||
setData={setNewDataset}
|
||||
hidden={["datasetType"]}
|
||||
/>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,83 +1,83 @@
|
||||
import RadioCard from "@/components/RadioCard";
|
||||
import { Input, Select, Form } from "antd";
|
||||
import { datasetTypes } from "../../dataset.const";
|
||||
import { useEffect, useState } from "react";
|
||||
import { queryDatasetTagsUsingGet } from "../../dataset.api";
|
||||
|
||||
export default function BasicInformation({
|
||||
data,
|
||||
setData,
|
||||
hidden = [],
|
||||
}: {
|
||||
data: any;
|
||||
setData: any;
|
||||
hidden?: string[];
|
||||
}) {
|
||||
const [tagOptions, setTagOptions] = useState<
|
||||
{
|
||||
label: JSX.Element;
|
||||
title: string;
|
||||
options: { label: JSX.Element; value: string }[];
|
||||
}[]
|
||||
>([]);
|
||||
|
||||
// 获取标签
|
||||
const fetchTags = async () => {
|
||||
if (hidden.includes("tags")) return;
|
||||
try {
|
||||
const { data } = await queryDatasetTagsUsingGet();
|
||||
const customTags = data.map((tag) => ({
|
||||
label: tag.name,
|
||||
value: tag.name,
|
||||
}));
|
||||
setTagOptions(customTags);
|
||||
} catch (error) {
|
||||
console.error("Error fetching tags: ", error);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchTags();
|
||||
}, []);
|
||||
return (
|
||||
<>
|
||||
<Form.Item
|
||||
label="名称"
|
||||
name="name"
|
||||
rules={[{ required: true, message: "请输入数据集名称" }]}
|
||||
>
|
||||
<Input placeholder="输入数据集名称" />
|
||||
</Form.Item>
|
||||
{!hidden.includes("description") && (
|
||||
<Form.Item name="description" label="描述">
|
||||
<Input.TextArea placeholder="描述数据集的用途和内容" rows={3} />
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
{/* 数据集类型选择 - 使用卡片形式 */}
|
||||
{!hidden.includes("datasetType") && (
|
||||
<Form.Item
|
||||
label="类型"
|
||||
name="datasetType"
|
||||
rules={[{ required: true, message: "请选择数据集类型" }]}
|
||||
>
|
||||
<RadioCard
|
||||
options={datasetTypes}
|
||||
value={data.type}
|
||||
onChange={(datasetType) => setData({ ...data, datasetType })}
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
{!hidden.includes("tags") && (
|
||||
<Form.Item label="标签" name="tags">
|
||||
<Select
|
||||
className="w-full"
|
||||
mode="tags"
|
||||
options={tagOptions}
|
||||
placeholder="请选择标签"
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
import RadioCard from "@/components/RadioCard";
|
||||
import { Input, Select, Form } from "antd";
|
||||
import { datasetTypes } from "../../dataset.const";
|
||||
import { useEffect, useState } from "react";
|
||||
import { queryDatasetTagsUsingGet } from "../../dataset.api";
|
||||
|
||||
export default function BasicInformation({
|
||||
data,
|
||||
setData,
|
||||
hidden = [],
|
||||
}: {
|
||||
data: any;
|
||||
setData: any;
|
||||
hidden?: string[];
|
||||
}) {
|
||||
const [tagOptions, setTagOptions] = useState<
|
||||
{
|
||||
label: JSX.Element;
|
||||
title: string;
|
||||
options: { label: JSX.Element; value: string }[];
|
||||
}[]
|
||||
>([]);
|
||||
|
||||
// 获取标签
|
||||
const fetchTags = async () => {
|
||||
if (hidden.includes("tags")) return;
|
||||
try {
|
||||
const { data } = await queryDatasetTagsUsingGet();
|
||||
const customTags = data.map((tag) => ({
|
||||
label: tag.name,
|
||||
value: tag.name,
|
||||
}));
|
||||
setTagOptions(customTags);
|
||||
} catch (error) {
|
||||
console.error("Error fetching tags: ", error);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchTags();
|
||||
}, []);
|
||||
return (
|
||||
<>
|
||||
<Form.Item
|
||||
label="名称"
|
||||
name="name"
|
||||
rules={[{ required: true, message: "请输入数据集名称" }]}
|
||||
>
|
||||
<Input placeholder="输入数据集名称" />
|
||||
</Form.Item>
|
||||
{!hidden.includes("description") && (
|
||||
<Form.Item name="description" label="描述">
|
||||
<Input.TextArea placeholder="描述数据集的用途和内容" rows={3} />
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
{/* 数据集类型选择 - 使用卡片形式 */}
|
||||
{!hidden.includes("datasetType") && (
|
||||
<Form.Item
|
||||
label="类型"
|
||||
name="datasetType"
|
||||
rules={[{ required: true, message: "请选择数据集类型" }]}
|
||||
>
|
||||
<RadioCard
|
||||
options={datasetTypes}
|
||||
value={data.type}
|
||||
onChange={(datasetType) => setData({ ...data, datasetType })}
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
{!hidden.includes("tags") && (
|
||||
<Form.Item label="标签" name="tags">
|
||||
<Select
|
||||
className="w-full"
|
||||
mode="tags"
|
||||
options={tagOptions}
|
||||
placeholder="请选择标签"
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,245 +1,245 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Breadcrumb, App, Tabs } from "antd";
|
||||
import {
|
||||
ReloadOutlined,
|
||||
DownloadOutlined,
|
||||
UploadOutlined,
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import DetailHeader from "@/components/DetailHeader";
|
||||
import { mapDataset, datasetTypeMap } from "../dataset.const";
|
||||
import type { Dataset } from "@/pages/DataManagement/dataset.model";
|
||||
import { Link, useNavigate, useParams } from "react-router";
|
||||
import { useFilesOperation } from "./useFilesOperation";
|
||||
import {
|
||||
createDatasetTagUsingPost,
|
||||
deleteDatasetByIdUsingDelete,
|
||||
downloadDatasetUsingGet,
|
||||
queryDatasetByIdUsingGet,
|
||||
queryDatasetTagsUsingGet,
|
||||
updateDatasetByIdUsingPut,
|
||||
} from "../dataset.api";
|
||||
import DataQuality from "./components/DataQuality";
|
||||
import DataLineageFlow from "./components/DataLineageFlow";
|
||||
import Overview from "./components/Overview";
|
||||
import { Activity, Clock, File, FileType } from "lucide-react";
|
||||
import EditDataset from "../Create/EditDataset";
|
||||
import ImportConfiguration from "./components/ImportConfiguration";
|
||||
|
||||
const tabList = [
|
||||
{
|
||||
key: "overview",
|
||||
label: "概览",
|
||||
},
|
||||
{
|
||||
key: "lineage",
|
||||
label: "数据血缘",
|
||||
},
|
||||
{
|
||||
key: "quality",
|
||||
label: "数据质量",
|
||||
},
|
||||
];
|
||||
|
||||
export default function DatasetDetail() {
|
||||
const { id } = useParams(); // 获取动态路由参数
|
||||
const navigate = useNavigate();
|
||||
const [activeTab, setActiveTab] = useState("overview");
|
||||
const { message } = App.useApp();
|
||||
const [showEditDialog, setShowEditDialog] = useState(false);
|
||||
|
||||
const [dataset, setDataset] = useState<Dataset>({} as Dataset);
|
||||
const filesOperation = useFilesOperation(dataset);
|
||||
|
||||
const [showUploadDialog, setShowUploadDialog] = useState(false);
|
||||
const navigateItems = useMemo(
|
||||
() => [
|
||||
{
|
||||
title: <Link to="/data/management">数据管理</Link>,
|
||||
},
|
||||
{
|
||||
title: dataset.name || "数据集详情",
|
||||
},
|
||||
],
|
||||
[dataset]
|
||||
);
|
||||
const fetchDataset = async () => {
|
||||
const { data } = await queryDatasetByIdUsingGet(id as unknown as number);
|
||||
setDataset(mapDataset(data));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchDataset();
|
||||
filesOperation.fetchFiles();
|
||||
}, []);
|
||||
|
||||
const handleRefresh = async (showMessage = true) => {
|
||||
fetchDataset();
|
||||
filesOperation.fetchFiles();
|
||||
if (showMessage) message.success({ content: "数据刷新成功" });
|
||||
};
|
||||
|
||||
const handleDownload = async () => {
|
||||
await downloadDatasetUsingGet(dataset.id);
|
||||
message.success("文件下载成功");
|
||||
};
|
||||
|
||||
const handleDeleteDataset = async () => {
|
||||
await deleteDatasetByIdUsingDelete(dataset.id);
|
||||
navigate("/data/management");
|
||||
message.success("数据集删除成功");
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const refreshData = () => {
|
||||
handleRefresh(false);
|
||||
};
|
||||
window.addEventListener("update:dataset", refreshData);
|
||||
return () => {
|
||||
window.removeEventListener("update:dataset", refreshData);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 基本信息描述项
|
||||
const statistics = [
|
||||
{
|
||||
icon: <File className="text-blue-400 w-4 h-4" />,
|
||||
key: "file",
|
||||
value: dataset?.fileCount || 0,
|
||||
},
|
||||
{
|
||||
icon: <Activity className="text-blue-400 w-4 h-4" />,
|
||||
key: "size",
|
||||
value: dataset?.size || "0 B",
|
||||
},
|
||||
{
|
||||
icon: <FileType className="text-blue-400 w-4 h-4" />,
|
||||
key: "type",
|
||||
value:
|
||||
datasetTypeMap[dataset?.datasetType as keyof typeof datasetTypeMap]
|
||||
?.label ||
|
||||
dataset?.type ||
|
||||
"未知",
|
||||
},
|
||||
{
|
||||
icon: <Clock className="text-blue-400 w-4 h-4" />,
|
||||
key: "time",
|
||||
value: dataset?.updatedAt,
|
||||
},
|
||||
];
|
||||
|
||||
// 数据集操作列表
|
||||
const operations = [
|
||||
{
|
||||
key: "edit",
|
||||
label: "编辑",
|
||||
icon: <EditOutlined />,
|
||||
onClick: () => {
|
||||
setShowEditDialog(true);
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
key: "upload",
|
||||
label: "导入数据",
|
||||
icon: <UploadOutlined />,
|
||||
onClick: () => setShowUploadDialog(true),
|
||||
},
|
||||
{
|
||||
key: "export",
|
||||
label: "导出",
|
||||
icon: <DownloadOutlined />,
|
||||
// isDropdown: true,
|
||||
// items: [
|
||||
// { key: "alpaca", label: "Alpaca 格式", icon: <FileTextOutlined /> },
|
||||
// { key: "jsonl", label: "JSONL 格式", icon: <DatabaseOutlined /> },
|
||||
// { key: "csv", label: "CSV 格式", icon: <FileTextOutlined /> },
|
||||
// { key: "coco", label: "COCO 格式", icon: <FileImageOutlined /> },
|
||||
// ],
|
||||
onClick: () => handleDownload(),
|
||||
},
|
||||
{
|
||||
key: "refresh",
|
||||
label: "刷新",
|
||||
icon: <ReloadOutlined />,
|
||||
onClick: handleRefresh,
|
||||
},
|
||||
{
|
||||
key: "delete",
|
||||
label: "删除",
|
||||
danger: true,
|
||||
confirm: {
|
||||
title: "确认删除该数据集?",
|
||||
description: "删除后该数据集将无法恢复,请谨慎操作。",
|
||||
okText: "删除",
|
||||
cancelText: "取消",
|
||||
okType: "danger",
|
||||
},
|
||||
icon: <DeleteOutlined />,
|
||||
onClick: handleDeleteDataset,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col gap-4">
|
||||
<Breadcrumb items={navigateItems} />
|
||||
{/* Header */}
|
||||
<DetailHeader
|
||||
data={dataset}
|
||||
statistics={statistics}
|
||||
operations={operations}
|
||||
tagConfig={{
|
||||
showAdd: true,
|
||||
tags: dataset.tags || [],
|
||||
onFetchTags: async () => {
|
||||
const res = await queryDatasetTagsUsingGet({
|
||||
page: 0,
|
||||
pageSize: 1000,
|
||||
});
|
||||
return res.data || [];
|
||||
},
|
||||
onCreateAndTag: async (tagName) => {
|
||||
const res = await createDatasetTagUsingPost({ name: tagName });
|
||||
if (res.data) {
|
||||
await updateDatasetByIdUsingPut(dataset.id, {
|
||||
tags: [...dataset.tags.map((tag) => tag.name), res.data.name],
|
||||
});
|
||||
handleRefresh();
|
||||
}
|
||||
},
|
||||
onAddTag: async (tag) => {
|
||||
const res = await updateDatasetByIdUsingPut(dataset.id, {
|
||||
tags: [...dataset.tags.map((tag) => tag.name), tag],
|
||||
});
|
||||
if (res.data) {
|
||||
handleRefresh();
|
||||
}
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<div className="flex-overflow-auto p-6 pt-2 bg-white rounded-md shadow">
|
||||
<Tabs activeKey={activeTab} items={tabList} onChange={setActiveTab} />
|
||||
<div className="h-full overflow-auto">
|
||||
{activeTab === "overview" && (
|
||||
<Overview dataset={dataset} filesOperation={filesOperation} fetchDataset={fetchDataset}/>
|
||||
)}
|
||||
{activeTab === "lineage" && <DataLineageFlow dataset={dataset} />}
|
||||
{activeTab === "quality" && <DataQuality />}
|
||||
</div>
|
||||
</div>
|
||||
<ImportConfiguration
|
||||
data={dataset}
|
||||
open={showUploadDialog}
|
||||
onClose={() => setShowUploadDialog(false)}
|
||||
updateEvent="update:dataset"
|
||||
/>
|
||||
<EditDataset
|
||||
data={dataset}
|
||||
open={showEditDialog}
|
||||
onClose={() => setShowEditDialog(false)}
|
||||
onRefresh={handleRefresh}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Breadcrumb, App, Tabs } from "antd";
|
||||
import {
|
||||
ReloadOutlined,
|
||||
DownloadOutlined,
|
||||
UploadOutlined,
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import DetailHeader from "@/components/DetailHeader";
|
||||
import { mapDataset, datasetTypeMap } from "../dataset.const";
|
||||
import type { Dataset } from "@/pages/DataManagement/dataset.model";
|
||||
import { Link, useNavigate, useParams } from "react-router";
|
||||
import { useFilesOperation } from "./useFilesOperation";
|
||||
import {
|
||||
createDatasetTagUsingPost,
|
||||
deleteDatasetByIdUsingDelete,
|
||||
downloadDatasetUsingGet,
|
||||
queryDatasetByIdUsingGet,
|
||||
queryDatasetTagsUsingGet,
|
||||
updateDatasetByIdUsingPut,
|
||||
} from "../dataset.api";
|
||||
import DataQuality from "./components/DataQuality";
|
||||
import DataLineageFlow from "./components/DataLineageFlow";
|
||||
import Overview from "./components/Overview";
|
||||
import { Activity, Clock, File, FileType } from "lucide-react";
|
||||
import EditDataset from "../Create/EditDataset";
|
||||
import ImportConfiguration from "./components/ImportConfiguration";
|
||||
|
||||
const tabList = [
|
||||
{
|
||||
key: "overview",
|
||||
label: "概览",
|
||||
},
|
||||
{
|
||||
key: "lineage",
|
||||
label: "数据血缘",
|
||||
},
|
||||
{
|
||||
key: "quality",
|
||||
label: "数据质量",
|
||||
},
|
||||
];
|
||||
|
||||
export default function DatasetDetail() {
|
||||
const { id } = useParams(); // 获取动态路由参数
|
||||
const navigate = useNavigate();
|
||||
const [activeTab, setActiveTab] = useState("overview");
|
||||
const { message } = App.useApp();
|
||||
const [showEditDialog, setShowEditDialog] = useState(false);
|
||||
|
||||
const [dataset, setDataset] = useState<Dataset>({} as Dataset);
|
||||
const filesOperation = useFilesOperation(dataset);
|
||||
|
||||
const [showUploadDialog, setShowUploadDialog] = useState(false);
|
||||
const navigateItems = useMemo(
|
||||
() => [
|
||||
{
|
||||
title: <Link to="/data/management">数据管理</Link>,
|
||||
},
|
||||
{
|
||||
title: dataset.name || "数据集详情",
|
||||
},
|
||||
],
|
||||
[dataset]
|
||||
);
|
||||
const fetchDataset = async () => {
|
||||
const { data } = await queryDatasetByIdUsingGet(id as unknown as number);
|
||||
setDataset(mapDataset(data));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchDataset();
|
||||
filesOperation.fetchFiles();
|
||||
}, []);
|
||||
|
||||
const handleRefresh = async (showMessage = true) => {
|
||||
fetchDataset();
|
||||
filesOperation.fetchFiles();
|
||||
if (showMessage) message.success({ content: "数据刷新成功" });
|
||||
};
|
||||
|
||||
const handleDownload = async () => {
|
||||
await downloadDatasetUsingGet(dataset.id);
|
||||
message.success("文件下载成功");
|
||||
};
|
||||
|
||||
const handleDeleteDataset = async () => {
|
||||
await deleteDatasetByIdUsingDelete(dataset.id);
|
||||
navigate("/data/management");
|
||||
message.success("数据集删除成功");
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const refreshData = () => {
|
||||
handleRefresh(false);
|
||||
};
|
||||
window.addEventListener("update:dataset", refreshData);
|
||||
return () => {
|
||||
window.removeEventListener("update:dataset", refreshData);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 基本信息描述项
|
||||
const statistics = [
|
||||
{
|
||||
icon: <File className="text-blue-400 w-4 h-4" />,
|
||||
key: "file",
|
||||
value: dataset?.fileCount || 0,
|
||||
},
|
||||
{
|
||||
icon: <Activity className="text-blue-400 w-4 h-4" />,
|
||||
key: "size",
|
||||
value: dataset?.size || "0 B",
|
||||
},
|
||||
{
|
||||
icon: <FileType className="text-blue-400 w-4 h-4" />,
|
||||
key: "type",
|
||||
value:
|
||||
datasetTypeMap[dataset?.datasetType as keyof typeof datasetTypeMap]
|
||||
?.label ||
|
||||
dataset?.type ||
|
||||
"未知",
|
||||
},
|
||||
{
|
||||
icon: <Clock className="text-blue-400 w-4 h-4" />,
|
||||
key: "time",
|
||||
value: dataset?.updatedAt,
|
||||
},
|
||||
];
|
||||
|
||||
// 数据集操作列表
|
||||
const operations = [
|
||||
{
|
||||
key: "edit",
|
||||
label: "编辑",
|
||||
icon: <EditOutlined />,
|
||||
onClick: () => {
|
||||
setShowEditDialog(true);
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
key: "upload",
|
||||
label: "导入数据",
|
||||
icon: <UploadOutlined />,
|
||||
onClick: () => setShowUploadDialog(true),
|
||||
},
|
||||
{
|
||||
key: "export",
|
||||
label: "导出",
|
||||
icon: <DownloadOutlined />,
|
||||
// isDropdown: true,
|
||||
// items: [
|
||||
// { key: "alpaca", label: "Alpaca 格式", icon: <FileTextOutlined /> },
|
||||
// { key: "jsonl", label: "JSONL 格式", icon: <DatabaseOutlined /> },
|
||||
// { key: "csv", label: "CSV 格式", icon: <FileTextOutlined /> },
|
||||
// { key: "coco", label: "COCO 格式", icon: <FileImageOutlined /> },
|
||||
// ],
|
||||
onClick: () => handleDownload(),
|
||||
},
|
||||
{
|
||||
key: "refresh",
|
||||
label: "刷新",
|
||||
icon: <ReloadOutlined />,
|
||||
onClick: handleRefresh,
|
||||
},
|
||||
{
|
||||
key: "delete",
|
||||
label: "删除",
|
||||
danger: true,
|
||||
confirm: {
|
||||
title: "确认删除该数据集?",
|
||||
description: "删除后该数据集将无法恢复,请谨慎操作。",
|
||||
okText: "删除",
|
||||
cancelText: "取消",
|
||||
okType: "danger",
|
||||
},
|
||||
icon: <DeleteOutlined />,
|
||||
onClick: handleDeleteDataset,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col gap-4">
|
||||
<Breadcrumb items={navigateItems} />
|
||||
{/* Header */}
|
||||
<DetailHeader
|
||||
data={dataset}
|
||||
statistics={statistics}
|
||||
operations={operations}
|
||||
tagConfig={{
|
||||
showAdd: true,
|
||||
tags: dataset.tags || [],
|
||||
onFetchTags: async () => {
|
||||
const res = await queryDatasetTagsUsingGet({
|
||||
page: 0,
|
||||
pageSize: 1000,
|
||||
});
|
||||
return res.data || [];
|
||||
},
|
||||
onCreateAndTag: async (tagName) => {
|
||||
const res = await createDatasetTagUsingPost({ name: tagName });
|
||||
if (res.data) {
|
||||
await updateDatasetByIdUsingPut(dataset.id, {
|
||||
tags: [...dataset.tags.map((tag) => tag.name), res.data.name],
|
||||
});
|
||||
handleRefresh();
|
||||
}
|
||||
},
|
||||
onAddTag: async (tag) => {
|
||||
const res = await updateDatasetByIdUsingPut(dataset.id, {
|
||||
tags: [...dataset.tags.map((tag) => tag.name), tag],
|
||||
});
|
||||
if (res.data) {
|
||||
handleRefresh();
|
||||
}
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<div className="flex-overflow-auto p-6 pt-2 bg-white rounded-md shadow">
|
||||
<Tabs activeKey={activeTab} items={tabList} onChange={setActiveTab} />
|
||||
<div className="h-full overflow-auto">
|
||||
{activeTab === "overview" && (
|
||||
<Overview dataset={dataset} filesOperation={filesOperation} fetchDataset={fetchDataset}/>
|
||||
)}
|
||||
{activeTab === "lineage" && <DataLineageFlow dataset={dataset} />}
|
||||
{activeTab === "quality" && <DataQuality />}
|
||||
</div>
|
||||
</div>
|
||||
<ImportConfiguration
|
||||
data={dataset}
|
||||
open={showUploadDialog}
|
||||
onClose={() => setShowUploadDialog(false)}
|
||||
updateEvent="update:dataset"
|
||||
/>
|
||||
<EditDataset
|
||||
data={dataset}
|
||||
open={showEditDialog}
|
||||
onClose={() => setShowEditDialog(false)}
|
||||
onRefresh={handleRefresh}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,52 +1,52 @@
|
||||
import DevelopmentInProgress from "@/components/DevelopmentInProgress";
|
||||
import { Dataset } from "../../dataset.model";
|
||||
|
||||
export default function DataLineageFlow(dataset: Dataset) {
|
||||
return <DevelopmentInProgress showHome={false} />
|
||||
const lineage = dataset.lineage;
|
||||
if (!lineage) return null;
|
||||
|
||||
const steps = [
|
||||
{ name: "数据源", value: lineage.source, icon: Database },
|
||||
...lineage.processing.map((step, index) => ({
|
||||
name: `处理${index + 1}`,
|
||||
value: step,
|
||||
icon: GitBranch,
|
||||
})),
|
||||
];
|
||||
|
||||
if (lineage.training) {
|
||||
steps.push({
|
||||
name: "模型训练",
|
||||
value: `${lineage.training.model} (准确率: ${lineage.training.accuracy}%)`,
|
||||
icon: Target,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="relative">
|
||||
{steps.map((step, index) => (
|
||||
<div key={index} className="flex items-start gap-4 pb-8 last:pb-0">
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="w-12 h-12 bg-gradient-to-br from-blue-500 to-indigo-600 rounded-full flex items-center justify-center shadow-lg">
|
||||
<step.icon className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
{index < steps.length - 1 && (
|
||||
<div className="w-0.5 h-12 bg-gradient-to-b from-blue-200 to-indigo-200 mt-2"></div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1 pt-3">
|
||||
<div className="bg-white rounded-lg border border-gray-200 p-4 shadow-sm hover:shadow-md transition-shadow">
|
||||
<h5 className="font-semibold text-gray-900 mb-1">
|
||||
{step.name}
|
||||
</h5>
|
||||
<p className="text-sm text-gray-600">{step.value}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
import DevelopmentInProgress from "@/components/DevelopmentInProgress";
|
||||
import { Dataset } from "../../dataset.model";
|
||||
|
||||
export default function DataLineageFlow(dataset: Dataset) {
|
||||
return <DevelopmentInProgress showHome={false} />
|
||||
const lineage = dataset.lineage;
|
||||
if (!lineage) return null;
|
||||
|
||||
const steps = [
|
||||
{ name: "数据源", value: lineage.source, icon: Database },
|
||||
...lineage.processing.map((step, index) => ({
|
||||
name: `处理${index + 1}`,
|
||||
value: step,
|
||||
icon: GitBranch,
|
||||
})),
|
||||
];
|
||||
|
||||
if (lineage.training) {
|
||||
steps.push({
|
||||
name: "模型训练",
|
||||
value: `${lineage.training.model} (准确率: ${lineage.training.accuracy}%)`,
|
||||
icon: Target,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="relative">
|
||||
{steps.map((step, index) => (
|
||||
<div key={index} className="flex items-start gap-4 pb-8 last:pb-0">
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="w-12 h-12 bg-gradient-to-br from-blue-500 to-indigo-600 rounded-full flex items-center justify-center shadow-lg">
|
||||
<step.icon className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
{index < steps.length - 1 && (
|
||||
<div className="w-0.5 h-12 bg-gradient-to-b from-blue-200 to-indigo-200 mt-2"></div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1 pt-3">
|
||||
<div className="bg-white rounded-lg border border-gray-200 p-4 shadow-sm hover:shadow-md transition-shadow">
|
||||
<h5 className="font-semibold text-gray-900 mb-1">
|
||||
{step.name}
|
||||
</h5>
|
||||
<p className="text-sm text-gray-600">{step.value}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,77 +1,77 @@
|
||||
import DevelopmentInProgress from "@/components/DevelopmentInProgress";
|
||||
import { Card } from "antd";
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
|
||||
export default function DataQuality() {
|
||||
return <DevelopmentInProgress showHome={false} />
|
||||
return (
|
||||
<div className=" mt-0">
|
||||
<div className="grid md:grid-cols-2 gap-6">
|
||||
<Card title="质量分布">
|
||||
{[
|
||||
{ metric: "图像清晰度", value: 96.2, color: "bg-green-500" },
|
||||
{ metric: "色彩一致性", value: 94.8, color: "bg-blue-500" },
|
||||
{ metric: "标注完整性", value: 98.1, color: "bg-purple-500" },
|
||||
].map((item, index) => (
|
||||
<div key={index} className="space-y-2">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span>{item.metric}</span>
|
||||
<span className="font-semibold">{item.value}%</span>
|
||||
</div>
|
||||
<div className="w-full bg-gray-200 rounded-full h-3">
|
||||
<div
|
||||
className={`${item.color} h-3 rounded-full transition-all duration-500`}
|
||||
style={{ width: `${item.value}%` }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</Card>
|
||||
|
||||
<Card title="数据完整性">
|
||||
{[
|
||||
{ metric: "文件完整性", value: 99.7, color: "bg-green-500" },
|
||||
{ metric: "元数据完整性", value: 97.3, color: "bg-blue-500" },
|
||||
{ metric: "标签一致性", value: 95.6, color: "bg-purple-500" },
|
||||
].map((item, index) => (
|
||||
<div key={index} className="space-y-2">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span>{item.metric}</span>
|
||||
<span className="font-semibold">{item.value}%</span>
|
||||
</div>
|
||||
<div className="w-full bg-gray-200 rounded-full h-3">
|
||||
<div
|
||||
className={`${item.color} h-3 rounded-full transition-all duration-500`}
|
||||
style={{ width: `${item.value}%` }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Card className="bg-gradient-to-r from-yellow-50 to-orange-50 border-yellow-200">
|
||||
<div className="flex items-start gap-4">
|
||||
<AlertTriangle className="w-6 h-6 text-yellow-600 mt-1 flex-shrink-0" />
|
||||
<div>
|
||||
<h4 className="font-semibold text-yellow-800 mb-2">质量改进建议</h4>
|
||||
<ul className="text-sm text-yellow-700 space-y-2">
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="w-1.5 h-1.5 bg-yellow-600 rounded-full mt-2 flex-shrink-0"></span>
|
||||
建议对42张图像进行重新标注以提高准确性
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="w-1.5 h-1.5 bg-yellow-600 rounded-full mt-2 flex-shrink-0"></span>
|
||||
检查并补充缺失的病理分级信息
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="w-1.5 h-1.5 bg-yellow-600 rounded-full mt-2 flex-shrink-0"></span>
|
||||
考虑增加更多低分化样本以平衡数据分布
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
import DevelopmentInProgress from "@/components/DevelopmentInProgress";
|
||||
import { Card } from "antd";
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
|
||||
export default function DataQuality() {
|
||||
return <DevelopmentInProgress showHome={false} />
|
||||
return (
|
||||
<div className=" mt-0">
|
||||
<div className="grid md:grid-cols-2 gap-6">
|
||||
<Card title="质量分布">
|
||||
{[
|
||||
{ metric: "图像清晰度", value: 96.2, color: "bg-green-500" },
|
||||
{ metric: "色彩一致性", value: 94.8, color: "bg-blue-500" },
|
||||
{ metric: "标注完整性", value: 98.1, color: "bg-purple-500" },
|
||||
].map((item, index) => (
|
||||
<div key={index} className="space-y-2">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span>{item.metric}</span>
|
||||
<span className="font-semibold">{item.value}%</span>
|
||||
</div>
|
||||
<div className="w-full bg-gray-200 rounded-full h-3">
|
||||
<div
|
||||
className={`${item.color} h-3 rounded-full transition-all duration-500`}
|
||||
style={{ width: `${item.value}%` }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</Card>
|
||||
|
||||
<Card title="数据完整性">
|
||||
{[
|
||||
{ metric: "文件完整性", value: 99.7, color: "bg-green-500" },
|
||||
{ metric: "元数据完整性", value: 97.3, color: "bg-blue-500" },
|
||||
{ metric: "标签一致性", value: 95.6, color: "bg-purple-500" },
|
||||
].map((item, index) => (
|
||||
<div key={index} className="space-y-2">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span>{item.metric}</span>
|
||||
<span className="font-semibold">{item.value}%</span>
|
||||
</div>
|
||||
<div className="w-full bg-gray-200 rounded-full h-3">
|
||||
<div
|
||||
className={`${item.color} h-3 rounded-full transition-all duration-500`}
|
||||
style={{ width: `${item.value}%` }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Card className="bg-gradient-to-r from-yellow-50 to-orange-50 border-yellow-200">
|
||||
<div className="flex items-start gap-4">
|
||||
<AlertTriangle className="w-6 h-6 text-yellow-600 mt-1 flex-shrink-0" />
|
||||
<div>
|
||||
<h4 className="font-semibold text-yellow-800 mb-2">质量改进建议</h4>
|
||||
<ul className="text-sm text-yellow-700 space-y-2">
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="w-1.5 h-1.5 bg-yellow-600 rounded-full mt-2 flex-shrink-0"></span>
|
||||
建议对42张图像进行重新标注以提高准确性
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="w-1.5 h-1.5 bg-yellow-600 rounded-full mt-2 flex-shrink-0"></span>
|
||||
检查并补充缺失的病理分级信息
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="w-1.5 h-1.5 bg-yellow-600 rounded-full mt-2 flex-shrink-0"></span>
|
||||
考虑增加更多低分化样本以平衡数据分布
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,277 +1,277 @@
|
||||
import { Select, Input, Form, Radio, Modal, Button, UploadFile, Switch } from "antd";
|
||||
import { InboxOutlined } from "@ant-design/icons";
|
||||
import { dataSourceOptions } from "../../dataset.const";
|
||||
import { Dataset, DataSource } from "../../dataset.model";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { queryTasksUsingGet } from "@/pages/DataCollection/collection.apis";
|
||||
import { updateDatasetByIdUsingPut } from "../../dataset.api";
|
||||
import { sliceFile } from "@/utils/file.util";
|
||||
import Dragger from "antd/es/upload/Dragger";
|
||||
|
||||
export default function ImportConfiguration({
|
||||
data,
|
||||
open,
|
||||
onClose,
|
||||
updateEvent = "update:dataset",
|
||||
}: {
|
||||
data: Dataset | null;
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
updateEvent?: string;
|
||||
}) {
|
||||
const [form] = Form.useForm();
|
||||
const [collectionOptions, setCollectionOptions] = useState([]);
|
||||
const [importConfig, setImportConfig] = useState<any>({
|
||||
source: DataSource.UPLOAD,
|
||||
});
|
||||
|
||||
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,
|
||||
updateEvent,
|
||||
hasArchive: importConfig.hasArchive,
|
||||
},
|
||||
})
|
||||
);
|
||||
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 () => {
|
||||
if (importConfig.source !== DataSource.COLLECTION) return;
|
||||
try {
|
||||
const res = await queryTasksUsingGet({ page: 0, size: 100 });
|
||||
const options = res.data.content.map((task: any) => ({
|
||||
label: task.name,
|
||||
value: task.id,
|
||||
}));
|
||||
setCollectionOptions(options);
|
||||
} catch (error) {
|
||||
console.error("Error fetching collection tasks:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const resetState = () => {
|
||||
form.resetFields();
|
||||
setFileList([]);
|
||||
form.setFieldsValue({ files: null });
|
||||
setImportConfig({ source: importConfig.source ? importConfig.source : DataSource.UPLOAD });
|
||||
};
|
||||
|
||||
const handleImportData = async () => {
|
||||
if (!data) return;
|
||||
if (importConfig.source === DataSource.UPLOAD) {
|
||||
await handleUpload(data);
|
||||
} else if (importConfig.source === DataSource.COLLECTION) {
|
||||
await updateDatasetByIdUsingPut(data.id, {
|
||||
...importConfig,
|
||||
});
|
||||
}
|
||||
onClose();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
resetState();
|
||||
fetchCollectionTasks();
|
||||
}
|
||||
}, [open, importConfig.source]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="导入数据"
|
||||
open={open}
|
||||
width={600}
|
||||
onCancel={() => {
|
||||
onClose();
|
||||
resetState();
|
||||
}}
|
||||
maskClosable={false}
|
||||
footer={
|
||||
<>
|
||||
<Button onClick={onClose}>取消</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
disabled={!fileList?.length && !importConfig.dataSource}
|
||||
onClick={handleImportData}
|
||||
>
|
||||
确定
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
initialValues={importConfig || {}}
|
||||
onValuesChange={(_, allValues) => setImportConfig(allValues)}
|
||||
>
|
||||
<Form.Item
|
||||
label="数据源"
|
||||
name="source"
|
||||
rules={[{ required: true, message: "请选择数据源" }]}
|
||||
>
|
||||
<Radio.Group
|
||||
buttonStyle="solid"
|
||||
options={dataSourceOptions}
|
||||
optionType="button"
|
||||
/>
|
||||
</Form.Item>
|
||||
{importConfig?.source === DataSource.COLLECTION && (
|
||||
<Form.Item name="dataSource" label="归集任务" required>
|
||||
<Select placeholder="请选择归集任务" options={collectionOptions} />
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
{/* obs import */}
|
||||
{importConfig?.source === DataSource.OBS && (
|
||||
<div className="grid grid-cols-2 gap-3 p-4 bg-blue-50 rounded-lg">
|
||||
<Form.Item
|
||||
name="endpoint"
|
||||
rules={[{ required: true }]}
|
||||
label="Endpoint"
|
||||
>
|
||||
<Input
|
||||
className="h-8 text-xs"
|
||||
placeholder="obs.cn-north-4.myhuaweicloud.com"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="bucket"
|
||||
rules={[{ required: true }]}
|
||||
label="Bucket"
|
||||
>
|
||||
<Input className="h-8 text-xs" placeholder="my-bucket" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="accessKey"
|
||||
rules={[{ required: true }]}
|
||||
label="Access Key"
|
||||
>
|
||||
<Input className="h-8 text-xs" placeholder="Access Key" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="secretKey"
|
||||
rules={[{ required: true }]}
|
||||
label="Secret Key"
|
||||
>
|
||||
<Input
|
||||
type="password"
|
||||
className="h-8 text-xs"
|
||||
placeholder="Secret Key"
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Local Upload Component */}
|
||||
{importConfig?.source === DataSource.UPLOAD && (
|
||||
<>
|
||||
<Form.Item
|
||||
label="自动解压上传的压缩包"
|
||||
name="hasArchive"
|
||||
valuePropName="checked"
|
||||
initialValue={true}
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="上传文件"
|
||||
name="files"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: "请上传文件",
|
||||
},
|
||||
]}
|
||||
>
|
||||
<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>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Target Configuration */}
|
||||
{importConfig?.target && importConfig?.target !== DataSource.UPLOAD && (
|
||||
<div className="space-y-3 p-4 bg-blue-50 rounded-lg">
|
||||
{importConfig?.target === DataSource.DATABASE && (
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<Form.Item
|
||||
name="databaseType"
|
||||
rules={[{ required: true }]}
|
||||
label="数据库类型"
|
||||
>
|
||||
<Select
|
||||
className="w-full"
|
||||
options={[
|
||||
{ label: "MySQL", value: "mysql" },
|
||||
{ label: "PostgreSQL", value: "postgresql" },
|
||||
{ label: "MongoDB", value: "mongodb" },
|
||||
]}
|
||||
></Select>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="tableName"
|
||||
rules={[{ required: true }]}
|
||||
label="表名"
|
||||
>
|
||||
<Input className="h-8 text-xs" placeholder="dataset_table" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="connectionString"
|
||||
rules={[{ required: true }]}
|
||||
label="连接字符串"
|
||||
>
|
||||
<Input
|
||||
className="h-8 text-xs col-span-2"
|
||||
placeholder="数据库连接字符串"
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
import { Select, Input, Form, Radio, Modal, Button, UploadFile, Switch } from "antd";
|
||||
import { InboxOutlined } from "@ant-design/icons";
|
||||
import { dataSourceOptions } from "../../dataset.const";
|
||||
import { Dataset, DataSource } from "../../dataset.model";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { queryTasksUsingGet } from "@/pages/DataCollection/collection.apis";
|
||||
import { updateDatasetByIdUsingPut } from "../../dataset.api";
|
||||
import { sliceFile } from "@/utils/file.util";
|
||||
import Dragger from "antd/es/upload/Dragger";
|
||||
|
||||
export default function ImportConfiguration({
|
||||
data,
|
||||
open,
|
||||
onClose,
|
||||
updateEvent = "update:dataset",
|
||||
}: {
|
||||
data: Dataset | null;
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
updateEvent?: string;
|
||||
}) {
|
||||
const [form] = Form.useForm();
|
||||
const [collectionOptions, setCollectionOptions] = useState([]);
|
||||
const [importConfig, setImportConfig] = useState<any>({
|
||||
source: DataSource.UPLOAD,
|
||||
});
|
||||
|
||||
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,
|
||||
updateEvent,
|
||||
hasArchive: importConfig.hasArchive,
|
||||
},
|
||||
})
|
||||
);
|
||||
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 () => {
|
||||
if (importConfig.source !== DataSource.COLLECTION) return;
|
||||
try {
|
||||
const res = await queryTasksUsingGet({ page: 0, size: 100 });
|
||||
const options = res.data.content.map((task: any) => ({
|
||||
label: task.name,
|
||||
value: task.id,
|
||||
}));
|
||||
setCollectionOptions(options);
|
||||
} catch (error) {
|
||||
console.error("Error fetching collection tasks:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const resetState = () => {
|
||||
form.resetFields();
|
||||
setFileList([]);
|
||||
form.setFieldsValue({ files: null });
|
||||
setImportConfig({ source: importConfig.source ? importConfig.source : DataSource.UPLOAD });
|
||||
};
|
||||
|
||||
const handleImportData = async () => {
|
||||
if (!data) return;
|
||||
if (importConfig.source === DataSource.UPLOAD) {
|
||||
await handleUpload(data);
|
||||
} else if (importConfig.source === DataSource.COLLECTION) {
|
||||
await updateDatasetByIdUsingPut(data.id, {
|
||||
...importConfig,
|
||||
});
|
||||
}
|
||||
onClose();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
resetState();
|
||||
fetchCollectionTasks();
|
||||
}
|
||||
}, [open, importConfig.source]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="导入数据"
|
||||
open={open}
|
||||
width={600}
|
||||
onCancel={() => {
|
||||
onClose();
|
||||
resetState();
|
||||
}}
|
||||
maskClosable={false}
|
||||
footer={
|
||||
<>
|
||||
<Button onClick={onClose}>取消</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
disabled={!fileList?.length && !importConfig.dataSource}
|
||||
onClick={handleImportData}
|
||||
>
|
||||
确定
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
initialValues={importConfig || {}}
|
||||
onValuesChange={(_, allValues) => setImportConfig(allValues)}
|
||||
>
|
||||
<Form.Item
|
||||
label="数据源"
|
||||
name="source"
|
||||
rules={[{ required: true, message: "请选择数据源" }]}
|
||||
>
|
||||
<Radio.Group
|
||||
buttonStyle="solid"
|
||||
options={dataSourceOptions}
|
||||
optionType="button"
|
||||
/>
|
||||
</Form.Item>
|
||||
{importConfig?.source === DataSource.COLLECTION && (
|
||||
<Form.Item name="dataSource" label="归集任务" required>
|
||||
<Select placeholder="请选择归集任务" options={collectionOptions} />
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
{/* obs import */}
|
||||
{importConfig?.source === DataSource.OBS && (
|
||||
<div className="grid grid-cols-2 gap-3 p-4 bg-blue-50 rounded-lg">
|
||||
<Form.Item
|
||||
name="endpoint"
|
||||
rules={[{ required: true }]}
|
||||
label="Endpoint"
|
||||
>
|
||||
<Input
|
||||
className="h-8 text-xs"
|
||||
placeholder="obs.cn-north-4.myhuaweicloud.com"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="bucket"
|
||||
rules={[{ required: true }]}
|
||||
label="Bucket"
|
||||
>
|
||||
<Input className="h-8 text-xs" placeholder="my-bucket" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="accessKey"
|
||||
rules={[{ required: true }]}
|
||||
label="Access Key"
|
||||
>
|
||||
<Input className="h-8 text-xs" placeholder="Access Key" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="secretKey"
|
||||
rules={[{ required: true }]}
|
||||
label="Secret Key"
|
||||
>
|
||||
<Input
|
||||
type="password"
|
||||
className="h-8 text-xs"
|
||||
placeholder="Secret Key"
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Local Upload Component */}
|
||||
{importConfig?.source === DataSource.UPLOAD && (
|
||||
<>
|
||||
<Form.Item
|
||||
label="自动解压上传的压缩包"
|
||||
name="hasArchive"
|
||||
valuePropName="checked"
|
||||
initialValue={true}
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="上传文件"
|
||||
name="files"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: "请上传文件",
|
||||
},
|
||||
]}
|
||||
>
|
||||
<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>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Target Configuration */}
|
||||
{importConfig?.target && importConfig?.target !== DataSource.UPLOAD && (
|
||||
<div className="space-y-3 p-4 bg-blue-50 rounded-lg">
|
||||
{importConfig?.target === DataSource.DATABASE && (
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<Form.Item
|
||||
name="databaseType"
|
||||
rules={[{ required: true }]}
|
||||
label="数据库类型"
|
||||
>
|
||||
<Select
|
||||
className="w-full"
|
||||
options={[
|
||||
{ label: "MySQL", value: "mysql" },
|
||||
{ label: "PostgreSQL", value: "postgresql" },
|
||||
{ label: "MongoDB", value: "mongodb" },
|
||||
]}
|
||||
></Select>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="tableName"
|
||||
rules={[{ required: true }]}
|
||||
label="表名"
|
||||
>
|
||||
<Input className="h-8 text-xs" placeholder="dataset_table" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="connectionString"
|
||||
rules={[{ required: true }]}
|
||||
label="连接字符串"
|
||||
>
|
||||
<Input
|
||||
className="h-8 text-xs col-span-2"
|
||||
placeholder="数据库连接字符串"
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,316 +1,316 @@
|
||||
import { Button, Descriptions, DescriptionsProps, Modal, Table } from "antd";
|
||||
import { formatBytes, formatDateTime } from "@/utils/unit";
|
||||
import { Download, Trash2, Folder, File } from "lucide-react";
|
||||
import { datasetTypeMap } from "../../dataset.const";
|
||||
|
||||
export default function Overview({ dataset, filesOperation, fetchDataset }) {
|
||||
const {
|
||||
fileList,
|
||||
pagination,
|
||||
selectedFiles,
|
||||
setSelectedFiles,
|
||||
previewVisible,
|
||||
previewFileName,
|
||||
previewContent,
|
||||
setPreviewVisible,
|
||||
handleDeleteFile,
|
||||
handleDownloadFile,
|
||||
handleBatchDeleteFiles,
|
||||
handleBatchExport,
|
||||
} = filesOperation;
|
||||
|
||||
// 文件列表多选配置
|
||||
const rowSelection = {
|
||||
onChange: (selectedRowKeys: React.Key[], selectedRows: any[]) => {
|
||||
setSelectedFiles(selectedRowKeys as number[]);
|
||||
console.log(
|
||||
`selectedRowKeys: ${selectedRowKeys}`,
|
||||
"selectedRows: ",
|
||||
selectedRows
|
||||
);
|
||||
},
|
||||
};
|
||||
// 基本信息
|
||||
const items: DescriptionsProps["items"] = [
|
||||
{
|
||||
key: "id",
|
||||
label: "ID",
|
||||
children: dataset.id,
|
||||
},
|
||||
{
|
||||
key: "name",
|
||||
label: "名称",
|
||||
children: dataset.name,
|
||||
},
|
||||
{
|
||||
key: "fileCount",
|
||||
label: "文件数",
|
||||
children: dataset.fileCount || 0,
|
||||
},
|
||||
{
|
||||
key: "size",
|
||||
label: "数据大小",
|
||||
children: dataset.size || "0 B",
|
||||
},
|
||||
|
||||
{
|
||||
key: "datasetType",
|
||||
label: "类型",
|
||||
children: datasetTypeMap[dataset?.datasetType]?.label || "未知",
|
||||
},
|
||||
{
|
||||
key: "status",
|
||||
label: "状态",
|
||||
children: dataset?.status?.label || "未知",
|
||||
},
|
||||
{
|
||||
key: "createdBy",
|
||||
label: "创建者",
|
||||
children: dataset.createdBy || "未知",
|
||||
},
|
||||
{
|
||||
key: "targetLocation",
|
||||
label: "存储路径",
|
||||
children: dataset.targetLocation || "未知",
|
||||
},
|
||||
{
|
||||
key: "pvcName",
|
||||
label: "存储名称",
|
||||
children: dataset.pvcName || "未知",
|
||||
},
|
||||
{
|
||||
key: "createdAt",
|
||||
label: "创建时间",
|
||||
children: dataset.createdAt,
|
||||
},
|
||||
{
|
||||
key: "updatedAt",
|
||||
label: "更新时间",
|
||||
children: dataset.updatedAt,
|
||||
},
|
||||
{
|
||||
key: "description",
|
||||
label: "描述",
|
||||
children: dataset.description || "无",
|
||||
},
|
||||
];
|
||||
|
||||
// 文件列表列定义
|
||||
const columns = [
|
||||
{
|
||||
title: "文件名",
|
||||
dataIndex: "fileName",
|
||||
key: "fileName",
|
||||
fixed: "left",
|
||||
render: (text: string, record: any) => {
|
||||
const isDirectory = record.id.startsWith('directory-');
|
||||
const iconSize = 16;
|
||||
|
||||
const content = (
|
||||
<div className="flex items-center">
|
||||
{isDirectory ? (
|
||||
<Folder className="mr-2 text-blue-500" size={iconSize} />
|
||||
) : (
|
||||
<File className="mr-2 text-black" size={iconSize} />
|
||||
)}
|
||||
<span className="truncate text-black">{text}</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (isDirectory) {
|
||||
return (
|
||||
<Button
|
||||
type="link"
|
||||
onClick={(e) => {
|
||||
const currentPath = filesOperation.pagination.prefix || '';
|
||||
const newPath = `${currentPath}${record.fileName}`;
|
||||
filesOperation.fetchFiles(newPath);
|
||||
}}
|
||||
>
|
||||
{content}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
type="link"
|
||||
onClick={(e) => {}}
|
||||
>
|
||||
{content}
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "大小",
|
||||
dataIndex: "fileSize",
|
||||
key: "fileSize",
|
||||
width: 150,
|
||||
render: (text: number, record: any) => {
|
||||
const isDirectory = record.id.startsWith('directory-');
|
||||
if (isDirectory) {
|
||||
return "-";
|
||||
}
|
||||
return formatBytes(text)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "上传时间",
|
||||
dataIndex: "uploadTime",
|
||||
key: "uploadTime",
|
||||
width: 200,
|
||||
render: (text) => formatDateTime(text),
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
key: "action",
|
||||
width: 180,
|
||||
fixed: "right",
|
||||
render: (_, record) => {
|
||||
const isDirectory = record.id.startsWith('directory-');
|
||||
if (isDirectory) {
|
||||
return <div className="flex"/>;
|
||||
}
|
||||
return (
|
||||
<div className="flex">
|
||||
<Button
|
||||
size="small"
|
||||
type="link"
|
||||
onClick={() => handleDownloadFile(record)}
|
||||
>
|
||||
下载
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
type="link"
|
||||
onClick={async () => {
|
||||
await handleDeleteFile(record);
|
||||
fetchDataset()
|
||||
}
|
||||
}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</div>
|
||||
)},
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className=" flex flex-col gap-4">
|
||||
{/* 基本信息 */}
|
||||
<Descriptions
|
||||
title="基本信息"
|
||||
layout="vertical"
|
||||
size="small"
|
||||
items={items}
|
||||
column={5}
|
||||
/>
|
||||
|
||||
{/* 文件列表 */}
|
||||
<h2 className="text-base font-semibold mt-8">文件列表</h2>
|
||||
{selectedFiles.length > 0 && (
|
||||
<div className="flex items-center gap-2 p-3 bg-blue-50 rounded-lg border border-blue-200">
|
||||
<span className="text-sm text-blue-700 font-medium">
|
||||
已选择 {selectedFiles.length} 个文件
|
||||
</span>
|
||||
<Button
|
||||
onClick={handleBatchExport}
|
||||
className="ml-auto bg-transparent"
|
||||
>
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
批量导出
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleBatchDeleteFiles}
|
||||
className="text-red-600 hover:text-red-700 hover:bg-red-50 bg-transparent"
|
||||
>
|
||||
<Trash2 className="w-4 h-4 mr-2" />
|
||||
批量删除
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<div className="overflow-x-auto">
|
||||
<div className="mb-2">
|
||||
{(filesOperation.pagination.prefix || '') !== '' && (
|
||||
<Button
|
||||
type="link"
|
||||
onClick={() => {
|
||||
// 获取上一级目录
|
||||
const currentPath = filesOperation.pagination.prefix || '';
|
||||
const pathParts = currentPath.split('/').filter(Boolean);
|
||||
pathParts.pop(); // 移除最后一个目录
|
||||
const parentPath = pathParts.length > 0 ? `${pathParts.join('/')}/` : '';
|
||||
filesOperation.fetchFiles(parentPath);
|
||||
}}
|
||||
className="p-0"
|
||||
>
|
||||
<span className="flex items-center text-blue-500">
|
||||
<svg
|
||||
className="w-4 h-4 mr-1"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M10 19l-7-7m0 0l7-7m-7 7h18"
|
||||
/>
|
||||
</svg>
|
||||
返回上一级
|
||||
</span>
|
||||
</Button>
|
||||
)}
|
||||
{filesOperation.pagination.prefix && (
|
||||
<span className="ml-2 text-gray-600">当前路径: {filesOperation.pagination.prefix}</span>
|
||||
)}
|
||||
</div>
|
||||
<Table
|
||||
size="middle"
|
||||
rowKey="id"
|
||||
columns={columns}
|
||||
dataSource={fileList}
|
||||
// rowSelection={rowSelection}
|
||||
scroll={{ x: "max-content", y: 600 }}
|
||||
pagination={{
|
||||
...pagination,
|
||||
showTotal: (total) => `共 ${total} 条`,
|
||||
onChange: (page, pageSize) => {
|
||||
filesOperation.setPagination(prev => ({
|
||||
...prev,
|
||||
current: page,
|
||||
pageSize: pageSize
|
||||
}));
|
||||
filesOperation.fetchFiles(pagination.prefix, page, pageSize);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/* 文件预览弹窗 */}
|
||||
<Modal
|
||||
title={`文件预览:${previewFileName}`}
|
||||
open={previewVisible}
|
||||
onCancel={() => setPreviewVisible(false)}
|
||||
footer={null}
|
||||
width={700}
|
||||
>
|
||||
<pre
|
||||
style={{
|
||||
whiteSpace: "pre-wrap",
|
||||
wordBreak: "break-all",
|
||||
fontSize: 14,
|
||||
color: "#222",
|
||||
}}
|
||||
>
|
||||
{previewContent}
|
||||
</pre>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
import { Button, Descriptions, DescriptionsProps, Modal, Table } from "antd";
|
||||
import { formatBytes, formatDateTime } from "@/utils/unit";
|
||||
import { Download, Trash2, Folder, File } from "lucide-react";
|
||||
import { datasetTypeMap } from "../../dataset.const";
|
||||
|
||||
export default function Overview({ dataset, filesOperation, fetchDataset }) {
|
||||
const {
|
||||
fileList,
|
||||
pagination,
|
||||
selectedFiles,
|
||||
setSelectedFiles,
|
||||
previewVisible,
|
||||
previewFileName,
|
||||
previewContent,
|
||||
setPreviewVisible,
|
||||
handleDeleteFile,
|
||||
handleDownloadFile,
|
||||
handleBatchDeleteFiles,
|
||||
handleBatchExport,
|
||||
} = filesOperation;
|
||||
|
||||
// 文件列表多选配置
|
||||
const rowSelection = {
|
||||
onChange: (selectedRowKeys: React.Key[], selectedRows: any[]) => {
|
||||
setSelectedFiles(selectedRowKeys as number[]);
|
||||
console.log(
|
||||
`selectedRowKeys: ${selectedRowKeys}`,
|
||||
"selectedRows: ",
|
||||
selectedRows
|
||||
);
|
||||
},
|
||||
};
|
||||
// 基本信息
|
||||
const items: DescriptionsProps["items"] = [
|
||||
{
|
||||
key: "id",
|
||||
label: "ID",
|
||||
children: dataset.id,
|
||||
},
|
||||
{
|
||||
key: "name",
|
||||
label: "名称",
|
||||
children: dataset.name,
|
||||
},
|
||||
{
|
||||
key: "fileCount",
|
||||
label: "文件数",
|
||||
children: dataset.fileCount || 0,
|
||||
},
|
||||
{
|
||||
key: "size",
|
||||
label: "数据大小",
|
||||
children: dataset.size || "0 B",
|
||||
},
|
||||
|
||||
{
|
||||
key: "datasetType",
|
||||
label: "类型",
|
||||
children: datasetTypeMap[dataset?.datasetType]?.label || "未知",
|
||||
},
|
||||
{
|
||||
key: "status",
|
||||
label: "状态",
|
||||
children: dataset?.status?.label || "未知",
|
||||
},
|
||||
{
|
||||
key: "createdBy",
|
||||
label: "创建者",
|
||||
children: dataset.createdBy || "未知",
|
||||
},
|
||||
{
|
||||
key: "targetLocation",
|
||||
label: "存储路径",
|
||||
children: dataset.targetLocation || "未知",
|
||||
},
|
||||
{
|
||||
key: "pvcName",
|
||||
label: "存储名称",
|
||||
children: dataset.pvcName || "未知",
|
||||
},
|
||||
{
|
||||
key: "createdAt",
|
||||
label: "创建时间",
|
||||
children: dataset.createdAt,
|
||||
},
|
||||
{
|
||||
key: "updatedAt",
|
||||
label: "更新时间",
|
||||
children: dataset.updatedAt,
|
||||
},
|
||||
{
|
||||
key: "description",
|
||||
label: "描述",
|
||||
children: dataset.description || "无",
|
||||
},
|
||||
];
|
||||
|
||||
// 文件列表列定义
|
||||
const columns = [
|
||||
{
|
||||
title: "文件名",
|
||||
dataIndex: "fileName",
|
||||
key: "fileName",
|
||||
fixed: "left",
|
||||
render: (text: string, record: any) => {
|
||||
const isDirectory = record.id.startsWith('directory-');
|
||||
const iconSize = 16;
|
||||
|
||||
const content = (
|
||||
<div className="flex items-center">
|
||||
{isDirectory ? (
|
||||
<Folder className="mr-2 text-blue-500" size={iconSize} />
|
||||
) : (
|
||||
<File className="mr-2 text-black" size={iconSize} />
|
||||
)}
|
||||
<span className="truncate text-black">{text}</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (isDirectory) {
|
||||
return (
|
||||
<Button
|
||||
type="link"
|
||||
onClick={(e) => {
|
||||
const currentPath = filesOperation.pagination.prefix || '';
|
||||
const newPath = `${currentPath}${record.fileName}`;
|
||||
filesOperation.fetchFiles(newPath);
|
||||
}}
|
||||
>
|
||||
{content}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
type="link"
|
||||
onClick={(e) => {}}
|
||||
>
|
||||
{content}
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "大小",
|
||||
dataIndex: "fileSize",
|
||||
key: "fileSize",
|
||||
width: 150,
|
||||
render: (text: number, record: any) => {
|
||||
const isDirectory = record.id.startsWith('directory-');
|
||||
if (isDirectory) {
|
||||
return "-";
|
||||
}
|
||||
return formatBytes(text)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "上传时间",
|
||||
dataIndex: "uploadTime",
|
||||
key: "uploadTime",
|
||||
width: 200,
|
||||
render: (text) => formatDateTime(text),
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
key: "action",
|
||||
width: 180,
|
||||
fixed: "right",
|
||||
render: (_, record) => {
|
||||
const isDirectory = record.id.startsWith('directory-');
|
||||
if (isDirectory) {
|
||||
return <div className="flex"/>;
|
||||
}
|
||||
return (
|
||||
<div className="flex">
|
||||
<Button
|
||||
size="small"
|
||||
type="link"
|
||||
onClick={() => handleDownloadFile(record)}
|
||||
>
|
||||
下载
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
type="link"
|
||||
onClick={async () => {
|
||||
await handleDeleteFile(record);
|
||||
fetchDataset()
|
||||
}
|
||||
}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</div>
|
||||
)},
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className=" flex flex-col gap-4">
|
||||
{/* 基本信息 */}
|
||||
<Descriptions
|
||||
title="基本信息"
|
||||
layout="vertical"
|
||||
size="small"
|
||||
items={items}
|
||||
column={5}
|
||||
/>
|
||||
|
||||
{/* 文件列表 */}
|
||||
<h2 className="text-base font-semibold mt-8">文件列表</h2>
|
||||
{selectedFiles.length > 0 && (
|
||||
<div className="flex items-center gap-2 p-3 bg-blue-50 rounded-lg border border-blue-200">
|
||||
<span className="text-sm text-blue-700 font-medium">
|
||||
已选择 {selectedFiles.length} 个文件
|
||||
</span>
|
||||
<Button
|
||||
onClick={handleBatchExport}
|
||||
className="ml-auto bg-transparent"
|
||||
>
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
批量导出
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleBatchDeleteFiles}
|
||||
className="text-red-600 hover:text-red-700 hover:bg-red-50 bg-transparent"
|
||||
>
|
||||
<Trash2 className="w-4 h-4 mr-2" />
|
||||
批量删除
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<div className="overflow-x-auto">
|
||||
<div className="mb-2">
|
||||
{(filesOperation.pagination.prefix || '') !== '' && (
|
||||
<Button
|
||||
type="link"
|
||||
onClick={() => {
|
||||
// 获取上一级目录
|
||||
const currentPath = filesOperation.pagination.prefix || '';
|
||||
const pathParts = currentPath.split('/').filter(Boolean);
|
||||
pathParts.pop(); // 移除最后一个目录
|
||||
const parentPath = pathParts.length > 0 ? `${pathParts.join('/')}/` : '';
|
||||
filesOperation.fetchFiles(parentPath);
|
||||
}}
|
||||
className="p-0"
|
||||
>
|
||||
<span className="flex items-center text-blue-500">
|
||||
<svg
|
||||
className="w-4 h-4 mr-1"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M10 19l-7-7m0 0l7-7m-7 7h18"
|
||||
/>
|
||||
</svg>
|
||||
返回上一级
|
||||
</span>
|
||||
</Button>
|
||||
)}
|
||||
{filesOperation.pagination.prefix && (
|
||||
<span className="ml-2 text-gray-600">当前路径: {filesOperation.pagination.prefix}</span>
|
||||
)}
|
||||
</div>
|
||||
<Table
|
||||
size="middle"
|
||||
rowKey="id"
|
||||
columns={columns}
|
||||
dataSource={fileList}
|
||||
// rowSelection={rowSelection}
|
||||
scroll={{ x: "max-content", y: 600 }}
|
||||
pagination={{
|
||||
...pagination,
|
||||
showTotal: (total) => `共 ${total} 条`,
|
||||
onChange: (page, pageSize) => {
|
||||
filesOperation.setPagination(prev => ({
|
||||
...prev,
|
||||
current: page,
|
||||
pageSize: pageSize
|
||||
}));
|
||||
filesOperation.fetchFiles(pagination.prefix, page, pageSize);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/* 文件预览弹窗 */}
|
||||
<Modal
|
||||
title={`文件预览:${previewFileName}`}
|
||||
open={previewVisible}
|
||||
onCancel={() => setPreviewVisible(false)}
|
||||
footer={null}
|
||||
width={700}
|
||||
>
|
||||
<pre
|
||||
style={{
|
||||
whiteSpace: "pre-wrap",
|
||||
wordBreak: "break-all",
|
||||
fontSize: 14,
|
||||
color: "#222",
|
||||
}}
|
||||
>
|
||||
{previewContent}
|
||||
</pre>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,149 +1,149 @@
|
||||
import type {
|
||||
Dataset,
|
||||
DatasetFile,
|
||||
} from "@/pages/DataManagement/dataset.model";
|
||||
import { App } from "antd";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
deleteDatasetFileUsingDelete,
|
||||
downloadFileByIdUsingGet,
|
||||
exportDatasetUsingPost,
|
||||
queryDatasetFilesUsingGet,
|
||||
} from "../dataset.api";
|
||||
import { useParams } from "react-router";
|
||||
|
||||
export function useFilesOperation(dataset: Dataset) {
|
||||
const { message } = App.useApp();
|
||||
const { id } = useParams(); // 获取动态路由参数
|
||||
|
||||
// 文件相关状态
|
||||
const [fileList, setFileList] = useState<DatasetFile[]>([]);
|
||||
const [selectedFiles, setSelectedFiles] = useState<number[]>([]);
|
||||
const [pagination, setPagination] = useState<{
|
||||
current: number;
|
||||
pageSize: number;
|
||||
total: number;
|
||||
prefix?: string;
|
||||
}>({ current: 1, pageSize: 10, total: 0, prefix: '' });
|
||||
|
||||
// 文件预览相关状态
|
||||
const [previewVisible, setPreviewVisible] = useState(false);
|
||||
const [previewContent, setPreviewContent] = useState("");
|
||||
const [previewFileName, setPreviewFileName] = useState("");
|
||||
|
||||
const fetchFiles = async (prefix: string = '', current, pageSize) => {
|
||||
const params: any = {
|
||||
page: current ? current : pagination.current,
|
||||
size: pageSize ? pageSize : pagination.pageSize,
|
||||
isWithDirectory: true,
|
||||
};
|
||||
|
||||
if (prefix !== undefined) {
|
||||
params.prefix = prefix;
|
||||
} else if (pagination.prefix) {
|
||||
params.prefix = pagination.prefix;
|
||||
}
|
||||
|
||||
const { data } = await queryDatasetFilesUsingGet(id!, params);
|
||||
setFileList(data.content || []);
|
||||
|
||||
// Update pagination with current prefix
|
||||
setPagination(prev => ({
|
||||
...prev,
|
||||
prefix: prefix !== undefined ? prefix : prev.prefix,
|
||||
total: data.totalElements || 0,
|
||||
}));
|
||||
};
|
||||
|
||||
const handleBatchDeleteFiles = () => {
|
||||
if (selectedFiles.length === 0) {
|
||||
message.warning({ content: "请先选择要删除的文件" });
|
||||
return;
|
||||
}
|
||||
// 执行批量删除逻辑
|
||||
selectedFiles.forEach(async (fileId) => {
|
||||
await fetch(`/api/datasets/${dataset.id}/files/${fileId}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
});
|
||||
fetchFiles(); // 刷新文件列表
|
||||
setSelectedFiles([]); // 清空选中状态
|
||||
message.success({
|
||||
content: `已删除 ${selectedFiles.length} 个文件`,
|
||||
});
|
||||
};
|
||||
|
||||
const handleDownloadFile = async (file: DatasetFile) => {
|
||||
// 实际导出逻辑
|
||||
await downloadFileByIdUsingGet(dataset.id, file.id, file.fileName);
|
||||
// 假设导出成功
|
||||
message.success({
|
||||
content: `已导出 1 个文件`,
|
||||
});
|
||||
setSelectedFiles([]); // 清空选中状态
|
||||
};
|
||||
|
||||
const handleShowFile = (file: any) => async () => {
|
||||
// 请求文件内容并弹窗预览
|
||||
try {
|
||||
const res = await fetch(`/api/datasets/${dataset.id}/file/${file.id}`);
|
||||
const data = await res.text();
|
||||
setPreviewFileName(file.fileName);
|
||||
setPreviewContent(data);
|
||||
setPreviewVisible(true);
|
||||
} catch (err) {
|
||||
message.error({ content: "文件预览失败" });
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteFile = async (file) => {
|
||||
try {
|
||||
await deleteDatasetFileUsingDelete(dataset.id, file.id);
|
||||
fetchFiles(); // 刷新文件列表
|
||||
message.success({ content: `文件 ${file.fileName} 已删除` });
|
||||
} catch (error) {
|
||||
message.error({ content: `文件 ${file.fileName} 删除失败` });
|
||||
}
|
||||
};
|
||||
|
||||
const handleBatchExport = () => {
|
||||
if (selectedFiles.length === 0) {
|
||||
message.warning({ content: "请先选择要导出的文件" });
|
||||
return;
|
||||
}
|
||||
// 执行批量导出逻辑
|
||||
exportDatasetUsingPost(dataset.id, { fileIds: selectedFiles })
|
||||
.then(() => {
|
||||
message.success({
|
||||
content: `已导出 ${selectedFiles.length} 个文件`,
|
||||
});
|
||||
setSelectedFiles([]); // 清空选中状态
|
||||
})
|
||||
.catch(() => {
|
||||
message.error({
|
||||
content: "导出失败,请稍后再试",
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
fileList,
|
||||
selectedFiles,
|
||||
setSelectedFiles,
|
||||
pagination,
|
||||
setPagination,
|
||||
previewVisible,
|
||||
setPreviewVisible,
|
||||
previewContent,
|
||||
previewFileName,
|
||||
setPreviewContent,
|
||||
setPreviewFileName,
|
||||
fetchFiles,
|
||||
setFileList,
|
||||
handleBatchDeleteFiles,
|
||||
handleDownloadFile,
|
||||
handleShowFile,
|
||||
handleDeleteFile,
|
||||
handleBatchExport,
|
||||
};
|
||||
}
|
||||
import type {
|
||||
Dataset,
|
||||
DatasetFile,
|
||||
} from "@/pages/DataManagement/dataset.model";
|
||||
import { App } from "antd";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
deleteDatasetFileUsingDelete,
|
||||
downloadFileByIdUsingGet,
|
||||
exportDatasetUsingPost,
|
||||
queryDatasetFilesUsingGet,
|
||||
} from "../dataset.api";
|
||||
import { useParams } from "react-router";
|
||||
|
||||
export function useFilesOperation(dataset: Dataset) {
|
||||
const { message } = App.useApp();
|
||||
const { id } = useParams(); // 获取动态路由参数
|
||||
|
||||
// 文件相关状态
|
||||
const [fileList, setFileList] = useState<DatasetFile[]>([]);
|
||||
const [selectedFiles, setSelectedFiles] = useState<number[]>([]);
|
||||
const [pagination, setPagination] = useState<{
|
||||
current: number;
|
||||
pageSize: number;
|
||||
total: number;
|
||||
prefix?: string;
|
||||
}>({ current: 1, pageSize: 10, total: 0, prefix: '' });
|
||||
|
||||
// 文件预览相关状态
|
||||
const [previewVisible, setPreviewVisible] = useState(false);
|
||||
const [previewContent, setPreviewContent] = useState("");
|
||||
const [previewFileName, setPreviewFileName] = useState("");
|
||||
|
||||
const fetchFiles = async (prefix: string = '', current, pageSize) => {
|
||||
const params: any = {
|
||||
page: current ? current : pagination.current,
|
||||
size: pageSize ? pageSize : pagination.pageSize,
|
||||
isWithDirectory: true,
|
||||
};
|
||||
|
||||
if (prefix !== undefined) {
|
||||
params.prefix = prefix;
|
||||
} else if (pagination.prefix) {
|
||||
params.prefix = pagination.prefix;
|
||||
}
|
||||
|
||||
const { data } = await queryDatasetFilesUsingGet(id!, params);
|
||||
setFileList(data.content || []);
|
||||
|
||||
// Update pagination with current prefix
|
||||
setPagination(prev => ({
|
||||
...prev,
|
||||
prefix: prefix !== undefined ? prefix : prev.prefix,
|
||||
total: data.totalElements || 0,
|
||||
}));
|
||||
};
|
||||
|
||||
const handleBatchDeleteFiles = () => {
|
||||
if (selectedFiles.length === 0) {
|
||||
message.warning({ content: "请先选择要删除的文件" });
|
||||
return;
|
||||
}
|
||||
// 执行批量删除逻辑
|
||||
selectedFiles.forEach(async (fileId) => {
|
||||
await fetch(`/api/datasets/${dataset.id}/files/${fileId}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
});
|
||||
fetchFiles(); // 刷新文件列表
|
||||
setSelectedFiles([]); // 清空选中状态
|
||||
message.success({
|
||||
content: `已删除 ${selectedFiles.length} 个文件`,
|
||||
});
|
||||
};
|
||||
|
||||
const handleDownloadFile = async (file: DatasetFile) => {
|
||||
// 实际导出逻辑
|
||||
await downloadFileByIdUsingGet(dataset.id, file.id, file.fileName);
|
||||
// 假设导出成功
|
||||
message.success({
|
||||
content: `已导出 1 个文件`,
|
||||
});
|
||||
setSelectedFiles([]); // 清空选中状态
|
||||
};
|
||||
|
||||
const handleShowFile = (file: any) => async () => {
|
||||
// 请求文件内容并弹窗预览
|
||||
try {
|
||||
const res = await fetch(`/api/datasets/${dataset.id}/file/${file.id}`);
|
||||
const data = await res.text();
|
||||
setPreviewFileName(file.fileName);
|
||||
setPreviewContent(data);
|
||||
setPreviewVisible(true);
|
||||
} catch (err) {
|
||||
message.error({ content: "文件预览失败" });
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteFile = async (file) => {
|
||||
try {
|
||||
await deleteDatasetFileUsingDelete(dataset.id, file.id);
|
||||
fetchFiles(); // 刷新文件列表
|
||||
message.success({ content: `文件 ${file.fileName} 已删除` });
|
||||
} catch (error) {
|
||||
message.error({ content: `文件 ${file.fileName} 删除失败` });
|
||||
}
|
||||
};
|
||||
|
||||
const handleBatchExport = () => {
|
||||
if (selectedFiles.length === 0) {
|
||||
message.warning({ content: "请先选择要导出的文件" });
|
||||
return;
|
||||
}
|
||||
// 执行批量导出逻辑
|
||||
exportDatasetUsingPost(dataset.id, { fileIds: selectedFiles })
|
||||
.then(() => {
|
||||
message.success({
|
||||
content: `已导出 ${selectedFiles.length} 个文件`,
|
||||
});
|
||||
setSelectedFiles([]); // 清空选中状态
|
||||
})
|
||||
.catch(() => {
|
||||
message.error({
|
||||
content: "导出失败,请稍后再试",
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
fileList,
|
||||
selectedFiles,
|
||||
setSelectedFiles,
|
||||
pagination,
|
||||
setPagination,
|
||||
previewVisible,
|
||||
setPreviewVisible,
|
||||
previewContent,
|
||||
previewFileName,
|
||||
setPreviewContent,
|
||||
setPreviewFileName,
|
||||
fetchFiles,
|
||||
setFileList,
|
||||
handleBatchDeleteFiles,
|
||||
handleDownloadFile,
|
||||
handleShowFile,
|
||||
handleDeleteFile,
|
||||
handleBatchExport,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,398 +1,398 @@
|
||||
import { Card, Button, Statistic, Table, Tooltip, Tag, App } from "antd";
|
||||
import {
|
||||
DownloadOutlined,
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
PlusOutlined,
|
||||
UploadOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import TagManager from "@/components/business/TagManagement";
|
||||
import { Link, useNavigate } from "react-router";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { SearchControls } from "@/components/SearchControls";
|
||||
import CardView from "@/components/CardView";
|
||||
import type { Dataset } from "@/pages/DataManagement/dataset.model";
|
||||
import { datasetStatusMap, datasetTypeMap, mapDataset } from "../dataset.const";
|
||||
import useFetchData from "@/hooks/useFetchData";
|
||||
import {
|
||||
downloadDatasetUsingGet,
|
||||
getDatasetStatisticsUsingGet,
|
||||
queryDatasetsUsingGet,
|
||||
deleteDatasetByIdUsingDelete,
|
||||
createDatasetTagUsingPost,
|
||||
queryDatasetTagsUsingGet,
|
||||
deleteDatasetTagUsingDelete,
|
||||
updateDatasetTagUsingPut,
|
||||
} from "../dataset.api";
|
||||
import { formatBytes } from "@/utils/unit";
|
||||
import EditDataset from "../Create/EditDataset";
|
||||
import ImportConfiguration from "../Detail/components/ImportConfiguration";
|
||||
|
||||
export default function DatasetManagementPage() {
|
||||
const navigate = useNavigate();
|
||||
const { message } = App.useApp();
|
||||
const [viewMode, setViewMode] = useState<"card" | "list">("card");
|
||||
const [editDatasetOpen, setEditDatasetOpen] = useState(false);
|
||||
const [currentDataset, setCurrentDataset] = useState<Dataset | null>(null);
|
||||
const [showUploadDialog, setShowUploadDialog] = useState(false);
|
||||
const [statisticsData, setStatisticsData] = useState<any>({
|
||||
count: {},
|
||||
size: {},
|
||||
});
|
||||
|
||||
async function fetchStatistics() {
|
||||
const { data } = await getDatasetStatisticsUsingGet();
|
||||
|
||||
const statistics = {
|
||||
size: [
|
||||
{
|
||||
title: "数据集总数",
|
||||
value: data?.totalDatasets || 0,
|
||||
},
|
||||
{
|
||||
title: "文件总数",
|
||||
value: data?.totalFiles || 0,
|
||||
},
|
||||
{
|
||||
title: "总大小",
|
||||
value: formatBytes(data?.totalSize) || '0 B',
|
||||
},
|
||||
],
|
||||
count: [
|
||||
{
|
||||
title: "文本",
|
||||
value: data?.count?.text || 0,
|
||||
},
|
||||
{
|
||||
title: "图像",
|
||||
value: data?.count?.image || 0,
|
||||
},
|
||||
{
|
||||
title: "音频",
|
||||
value: data?.count?.audio || 0,
|
||||
},
|
||||
{
|
||||
title: "视频",
|
||||
value: data?.count?.video || 0,
|
||||
},
|
||||
],
|
||||
};
|
||||
setStatisticsData(statistics);
|
||||
}
|
||||
|
||||
const [tags, setTags] = useState<string[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchTags = async () => {
|
||||
const { data } = await queryDatasetTagsUsingGet();
|
||||
setTags(data.map((tag) => tag.name));
|
||||
};
|
||||
fetchTags();
|
||||
}, []);
|
||||
|
||||
const filterOptions = useMemo(
|
||||
() => [
|
||||
{
|
||||
key: "type",
|
||||
label: "类型",
|
||||
options: [...Object.values(datasetTypeMap)],
|
||||
},
|
||||
{
|
||||
key: "status",
|
||||
label: "状态",
|
||||
options: [...Object.values(datasetStatusMap)],
|
||||
},
|
||||
{
|
||||
key: "tags",
|
||||
label: "标签",
|
||||
mode: "multiple",
|
||||
options: tags.map((tag) => ({ label: tag, value: tag })),
|
||||
},
|
||||
],
|
||||
[tags]
|
||||
);
|
||||
|
||||
const {
|
||||
loading,
|
||||
tableData,
|
||||
searchParams,
|
||||
pagination,
|
||||
fetchData,
|
||||
setSearchParams,
|
||||
handleFiltersChange,
|
||||
handleKeywordChange,
|
||||
} = useFetchData<Dataset>(
|
||||
queryDatasetsUsingGet,
|
||||
mapDataset,
|
||||
30000, // 30秒轮询间隔
|
||||
true, // 自动刷新
|
||||
[fetchStatistics], // 额外的轮询函数
|
||||
0
|
||||
);
|
||||
|
||||
const handleDownloadDataset = async (dataset: Dataset) => {
|
||||
await downloadDatasetUsingGet(dataset.id, dataset.name);
|
||||
message.success("数据集下载成功");
|
||||
};
|
||||
|
||||
const handleDeleteDataset = async (id: number) => {
|
||||
if (!id) return;
|
||||
await deleteDatasetByIdUsingDelete(id);
|
||||
fetchData({ pageOffset: 0 });
|
||||
message.success("数据删除成功");
|
||||
};
|
||||
|
||||
const handleImportData = (dataset: Dataset) => {
|
||||
setCurrentDataset(dataset);
|
||||
setShowUploadDialog(true);
|
||||
};
|
||||
|
||||
const handleRefresh = async (showMessage = true) => {
|
||||
await fetchData({ pageOffset: 0 });
|
||||
if (showMessage) {
|
||||
message.success("数据已刷新");
|
||||
}
|
||||
};
|
||||
|
||||
const operations = [
|
||||
{
|
||||
key: "edit",
|
||||
label: "编辑",
|
||||
icon: <EditOutlined />,
|
||||
onClick: (item: Dataset) => {
|
||||
setCurrentDataset(item);
|
||||
setEditDatasetOpen(true);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "import",
|
||||
label: "导入",
|
||||
icon: <UploadOutlined />,
|
||||
onClick: (item: Dataset) => {
|
||||
handleImportData(item);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "download",
|
||||
label: "下载",
|
||||
icon: <DownloadOutlined />,
|
||||
onClick: (item: Dataset) => {
|
||||
if (!item.id) return;
|
||||
handleDownloadDataset(item);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "delete",
|
||||
label: "删除",
|
||||
danger: true,
|
||||
confirm: {
|
||||
title: "确认删除该数据集?",
|
||||
description: "删除后该数据集将无法恢复,请谨慎操作。",
|
||||
okText: "删除",
|
||||
cancelText: "取消",
|
||||
okType: "danger",
|
||||
},
|
||||
icon: <DeleteOutlined />,
|
||||
onClick: (item: Dataset) => handleDeleteDataset(item.id),
|
||||
},
|
||||
];
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: "名称",
|
||||
dataIndex: "name",
|
||||
key: "name",
|
||||
fixed: "left",
|
||||
render: (name, record) => (
|
||||
<Button
|
||||
type="link"
|
||||
onClick={() => navigate(`/data/management/detail/${record.id}`)}
|
||||
>
|
||||
{name}
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "类型",
|
||||
dataIndex: "type",
|
||||
key: "type",
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: "状态",
|
||||
dataIndex: "status",
|
||||
key: "status",
|
||||
render: (status: any) => {
|
||||
return (
|
||||
<Tag icon={status?.icon} color={status?.color}>
|
||||
{status?.label}
|
||||
</Tag>
|
||||
);
|
||||
},
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: "大小",
|
||||
dataIndex: "size",
|
||||
key: "size",
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: "文件数",
|
||||
dataIndex: "fileCount",
|
||||
key: "fileCount",
|
||||
width: 100,
|
||||
},
|
||||
// {
|
||||
// title: "创建者",
|
||||
// dataIndex: "createdBy",
|
||||
// key: "createdBy",
|
||||
// width: 120,
|
||||
// },
|
||||
{
|
||||
title: "存储路径",
|
||||
dataIndex: "targetLocation",
|
||||
key: "targetLocation",
|
||||
width: 200,
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: "创建时间",
|
||||
dataIndex: "createdAt",
|
||||
key: "createdAt",
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
title: "更新时间",
|
||||
dataIndex: "updatedAt",
|
||||
key: "updatedAt",
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
key: "actions",
|
||||
width: 200,
|
||||
fixed: "right",
|
||||
render: (_: any, record: Dataset) => (
|
||||
<div className="flex items-center gap-2">
|
||||
{operations.map((op) => (
|
||||
<Tooltip key={op.key} title={op.label}>
|
||||
<Button
|
||||
type="text"
|
||||
icon={op.icon}
|
||||
onClick={() => op.onClick(record)}
|
||||
/>
|
||||
</Tooltip>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const renderCardView = () => (
|
||||
<CardView
|
||||
loading={loading}
|
||||
data={tableData}
|
||||
pageSize={9}
|
||||
operations={operations}
|
||||
pagination={pagination}
|
||||
onView={(dataset) => {
|
||||
navigate("/data/management/detail/" + dataset.id);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const renderListView = () => (
|
||||
<Card>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={tableData}
|
||||
pagination={pagination}
|
||||
rowKey="id"
|
||||
scroll={{ x: "max-content", y: "calc(100vh - 30rem)" }}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const refresh = () => {
|
||||
handleRefresh(true);
|
||||
};
|
||||
window.addEventListener("update:datasets", refresh);
|
||||
return () => {
|
||||
window.removeEventListener("update:datasets", refresh);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="gap-4 h-full flex flex-col">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="text-xl font-bold">数据管理</h1>
|
||||
<div className="flex gap-2 items-center">
|
||||
{/* tasks */}
|
||||
<TagManager
|
||||
onCreate={createDatasetTagUsingPost}
|
||||
onDelete={(ids: string) => deleteDatasetTagUsingDelete({ ids })}
|
||||
onUpdate={updateDatasetTagUsingPut}
|
||||
onFetch={queryDatasetTagsUsingGet}
|
||||
/>
|
||||
<Link to="/data/management/create">
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined className="w-4 h-4 mr-2" />}
|
||||
>
|
||||
创建数据集
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Statistics */}
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
<Card>
|
||||
<div className="grid grid-cols-3">
|
||||
{statisticsData.size?.map?.((item) => (
|
||||
<Statistic
|
||||
title={item.title}
|
||||
key={item.title}
|
||||
value={`${item.value}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
<SearchControls
|
||||
searchTerm={searchParams.keyword}
|
||||
onSearchChange={handleKeywordChange}
|
||||
searchPlaceholder="搜索数据集名称、描述"
|
||||
filters={filterOptions}
|
||||
onFiltersChange={handleFiltersChange}
|
||||
onClearFilters={() => setSearchParams({ ...searchParams, filter: {} })}
|
||||
viewMode={viewMode}
|
||||
onViewModeChange={setViewMode}
|
||||
showViewToggle
|
||||
onReload={handleRefresh}
|
||||
/>
|
||||
{viewMode === "card" ? renderCardView() : renderListView()}
|
||||
<EditDataset
|
||||
open={editDatasetOpen}
|
||||
data={currentDataset}
|
||||
onClose={() => {
|
||||
setCurrentDataset(null);
|
||||
setEditDatasetOpen(false);
|
||||
}}
|
||||
onRefresh={handleRefresh}
|
||||
/>
|
||||
<ImportConfiguration
|
||||
data={currentDataset}
|
||||
open={showUploadDialog}
|
||||
onClose={() => {
|
||||
setCurrentDataset(null);
|
||||
setShowUploadDialog(false);
|
||||
}}
|
||||
updateEvent="update:datasets"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
import { Card, Button, Statistic, Table, Tooltip, Tag, App } from "antd";
|
||||
import {
|
||||
DownloadOutlined,
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
PlusOutlined,
|
||||
UploadOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import TagManager from "@/components/business/TagManagement";
|
||||
import { Link, useNavigate } from "react-router";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { SearchControls } from "@/components/SearchControls";
|
||||
import CardView from "@/components/CardView";
|
||||
import type { Dataset } from "@/pages/DataManagement/dataset.model";
|
||||
import { datasetStatusMap, datasetTypeMap, mapDataset } from "../dataset.const";
|
||||
import useFetchData from "@/hooks/useFetchData";
|
||||
import {
|
||||
downloadDatasetUsingGet,
|
||||
getDatasetStatisticsUsingGet,
|
||||
queryDatasetsUsingGet,
|
||||
deleteDatasetByIdUsingDelete,
|
||||
createDatasetTagUsingPost,
|
||||
queryDatasetTagsUsingGet,
|
||||
deleteDatasetTagUsingDelete,
|
||||
updateDatasetTagUsingPut,
|
||||
} from "../dataset.api";
|
||||
import { formatBytes } from "@/utils/unit";
|
||||
import EditDataset from "../Create/EditDataset";
|
||||
import ImportConfiguration from "../Detail/components/ImportConfiguration";
|
||||
|
||||
export default function DatasetManagementPage() {
|
||||
const navigate = useNavigate();
|
||||
const { message } = App.useApp();
|
||||
const [viewMode, setViewMode] = useState<"card" | "list">("card");
|
||||
const [editDatasetOpen, setEditDatasetOpen] = useState(false);
|
||||
const [currentDataset, setCurrentDataset] = useState<Dataset | null>(null);
|
||||
const [showUploadDialog, setShowUploadDialog] = useState(false);
|
||||
const [statisticsData, setStatisticsData] = useState<any>({
|
||||
count: {},
|
||||
size: {},
|
||||
});
|
||||
|
||||
async function fetchStatistics() {
|
||||
const { data } = await getDatasetStatisticsUsingGet();
|
||||
|
||||
const statistics = {
|
||||
size: [
|
||||
{
|
||||
title: "数据集总数",
|
||||
value: data?.totalDatasets || 0,
|
||||
},
|
||||
{
|
||||
title: "文件总数",
|
||||
value: data?.totalFiles || 0,
|
||||
},
|
||||
{
|
||||
title: "总大小",
|
||||
value: formatBytes(data?.totalSize) || '0 B',
|
||||
},
|
||||
],
|
||||
count: [
|
||||
{
|
||||
title: "文本",
|
||||
value: data?.count?.text || 0,
|
||||
},
|
||||
{
|
||||
title: "图像",
|
||||
value: data?.count?.image || 0,
|
||||
},
|
||||
{
|
||||
title: "音频",
|
||||
value: data?.count?.audio || 0,
|
||||
},
|
||||
{
|
||||
title: "视频",
|
||||
value: data?.count?.video || 0,
|
||||
},
|
||||
],
|
||||
};
|
||||
setStatisticsData(statistics);
|
||||
}
|
||||
|
||||
const [tags, setTags] = useState<string[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchTags = async () => {
|
||||
const { data } = await queryDatasetTagsUsingGet();
|
||||
setTags(data.map((tag) => tag.name));
|
||||
};
|
||||
fetchTags();
|
||||
}, []);
|
||||
|
||||
const filterOptions = useMemo(
|
||||
() => [
|
||||
{
|
||||
key: "type",
|
||||
label: "类型",
|
||||
options: [...Object.values(datasetTypeMap)],
|
||||
},
|
||||
{
|
||||
key: "status",
|
||||
label: "状态",
|
||||
options: [...Object.values(datasetStatusMap)],
|
||||
},
|
||||
{
|
||||
key: "tags",
|
||||
label: "标签",
|
||||
mode: "multiple",
|
||||
options: tags.map((tag) => ({ label: tag, value: tag })),
|
||||
},
|
||||
],
|
||||
[tags]
|
||||
);
|
||||
|
||||
const {
|
||||
loading,
|
||||
tableData,
|
||||
searchParams,
|
||||
pagination,
|
||||
fetchData,
|
||||
setSearchParams,
|
||||
handleFiltersChange,
|
||||
handleKeywordChange,
|
||||
} = useFetchData<Dataset>(
|
||||
queryDatasetsUsingGet,
|
||||
mapDataset,
|
||||
30000, // 30秒轮询间隔
|
||||
true, // 自动刷新
|
||||
[fetchStatistics], // 额外的轮询函数
|
||||
0
|
||||
);
|
||||
|
||||
const handleDownloadDataset = async (dataset: Dataset) => {
|
||||
await downloadDatasetUsingGet(dataset.id, dataset.name);
|
||||
message.success("数据集下载成功");
|
||||
};
|
||||
|
||||
const handleDeleteDataset = async (id: number) => {
|
||||
if (!id) return;
|
||||
await deleteDatasetByIdUsingDelete(id);
|
||||
fetchData({ pageOffset: 0 });
|
||||
message.success("数据删除成功");
|
||||
};
|
||||
|
||||
const handleImportData = (dataset: Dataset) => {
|
||||
setCurrentDataset(dataset);
|
||||
setShowUploadDialog(true);
|
||||
};
|
||||
|
||||
const handleRefresh = async (showMessage = true) => {
|
||||
await fetchData({ pageOffset: 0 });
|
||||
if (showMessage) {
|
||||
message.success("数据已刷新");
|
||||
}
|
||||
};
|
||||
|
||||
const operations = [
|
||||
{
|
||||
key: "edit",
|
||||
label: "编辑",
|
||||
icon: <EditOutlined />,
|
||||
onClick: (item: Dataset) => {
|
||||
setCurrentDataset(item);
|
||||
setEditDatasetOpen(true);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "import",
|
||||
label: "导入",
|
||||
icon: <UploadOutlined />,
|
||||
onClick: (item: Dataset) => {
|
||||
handleImportData(item);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "download",
|
||||
label: "下载",
|
||||
icon: <DownloadOutlined />,
|
||||
onClick: (item: Dataset) => {
|
||||
if (!item.id) return;
|
||||
handleDownloadDataset(item);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "delete",
|
||||
label: "删除",
|
||||
danger: true,
|
||||
confirm: {
|
||||
title: "确认删除该数据集?",
|
||||
description: "删除后该数据集将无法恢复,请谨慎操作。",
|
||||
okText: "删除",
|
||||
cancelText: "取消",
|
||||
okType: "danger",
|
||||
},
|
||||
icon: <DeleteOutlined />,
|
||||
onClick: (item: Dataset) => handleDeleteDataset(item.id),
|
||||
},
|
||||
];
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: "名称",
|
||||
dataIndex: "name",
|
||||
key: "name",
|
||||
fixed: "left",
|
||||
render: (name, record) => (
|
||||
<Button
|
||||
type="link"
|
||||
onClick={() => navigate(`/data/management/detail/${record.id}`)}
|
||||
>
|
||||
{name}
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "类型",
|
||||
dataIndex: "type",
|
||||
key: "type",
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: "状态",
|
||||
dataIndex: "status",
|
||||
key: "status",
|
||||
render: (status: any) => {
|
||||
return (
|
||||
<Tag icon={status?.icon} color={status?.color}>
|
||||
{status?.label}
|
||||
</Tag>
|
||||
);
|
||||
},
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: "大小",
|
||||
dataIndex: "size",
|
||||
key: "size",
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: "文件数",
|
||||
dataIndex: "fileCount",
|
||||
key: "fileCount",
|
||||
width: 100,
|
||||
},
|
||||
// {
|
||||
// title: "创建者",
|
||||
// dataIndex: "createdBy",
|
||||
// key: "createdBy",
|
||||
// width: 120,
|
||||
// },
|
||||
{
|
||||
title: "存储路径",
|
||||
dataIndex: "targetLocation",
|
||||
key: "targetLocation",
|
||||
width: 200,
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: "创建时间",
|
||||
dataIndex: "createdAt",
|
||||
key: "createdAt",
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
title: "更新时间",
|
||||
dataIndex: "updatedAt",
|
||||
key: "updatedAt",
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
key: "actions",
|
||||
width: 200,
|
||||
fixed: "right",
|
||||
render: (_: any, record: Dataset) => (
|
||||
<div className="flex items-center gap-2">
|
||||
{operations.map((op) => (
|
||||
<Tooltip key={op.key} title={op.label}>
|
||||
<Button
|
||||
type="text"
|
||||
icon={op.icon}
|
||||
onClick={() => op.onClick(record)}
|
||||
/>
|
||||
</Tooltip>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const renderCardView = () => (
|
||||
<CardView
|
||||
loading={loading}
|
||||
data={tableData}
|
||||
pageSize={9}
|
||||
operations={operations}
|
||||
pagination={pagination}
|
||||
onView={(dataset) => {
|
||||
navigate("/data/management/detail/" + dataset.id);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const renderListView = () => (
|
||||
<Card>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={tableData}
|
||||
pagination={pagination}
|
||||
rowKey="id"
|
||||
scroll={{ x: "max-content", y: "calc(100vh - 30rem)" }}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const refresh = () => {
|
||||
handleRefresh(true);
|
||||
};
|
||||
window.addEventListener("update:datasets", refresh);
|
||||
return () => {
|
||||
window.removeEventListener("update:datasets", refresh);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="gap-4 h-full flex flex-col">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="text-xl font-bold">数据管理</h1>
|
||||
<div className="flex gap-2 items-center">
|
||||
{/* tasks */}
|
||||
<TagManager
|
||||
onCreate={createDatasetTagUsingPost}
|
||||
onDelete={(ids: string) => deleteDatasetTagUsingDelete({ ids })}
|
||||
onUpdate={updateDatasetTagUsingPut}
|
||||
onFetch={queryDatasetTagsUsingGet}
|
||||
/>
|
||||
<Link to="/data/management/create">
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined className="w-4 h-4 mr-2" />}
|
||||
>
|
||||
创建数据集
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Statistics */}
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
<Card>
|
||||
<div className="grid grid-cols-3">
|
||||
{statisticsData.size?.map?.((item) => (
|
||||
<Statistic
|
||||
title={item.title}
|
||||
key={item.title}
|
||||
value={`${item.value}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
<SearchControls
|
||||
searchTerm={searchParams.keyword}
|
||||
onSearchChange={handleKeywordChange}
|
||||
searchPlaceholder="搜索数据集名称、描述"
|
||||
filters={filterOptions}
|
||||
onFiltersChange={handleFiltersChange}
|
||||
onClearFilters={() => setSearchParams({ ...searchParams, filter: {} })}
|
||||
viewMode={viewMode}
|
||||
onViewModeChange={setViewMode}
|
||||
showViewToggle
|
||||
onReload={handleRefresh}
|
||||
/>
|
||||
{viewMode === "card" ? renderCardView() : renderListView()}
|
||||
<EditDataset
|
||||
open={editDatasetOpen}
|
||||
data={currentDataset}
|
||||
onClose={() => {
|
||||
setCurrentDataset(null);
|
||||
setEditDatasetOpen(false);
|
||||
}}
|
||||
onRefresh={handleRefresh}
|
||||
/>
|
||||
<ImportConfiguration
|
||||
data={currentDataset}
|
||||
open={showUploadDialog}
|
||||
onClose={() => {
|
||||
setCurrentDataset(null);
|
||||
setShowUploadDialog(false);
|
||||
}}
|
||||
updateEvent="update:datasets"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,191 +1,191 @@
|
||||
import { get, post, put, del, download } from "@/utils/request";
|
||||
|
||||
// 数据集统计接口
|
||||
export function getDatasetStatisticsUsingGet() {
|
||||
return get("/api/data-management/datasets/statistics");
|
||||
}
|
||||
|
||||
export function queryDatasetStatisticsByIdUsingGet(id: string | number) {
|
||||
return get(`/api/data-management/datasets/${id}/statistics`);
|
||||
}
|
||||
|
||||
// 查询数据集列表
|
||||
export function queryDatasetsUsingGet(params?: any) {
|
||||
return get("/api/data-management/datasets", params);
|
||||
}
|
||||
|
||||
// 创建数据集
|
||||
export function createDatasetUsingPost(data: any) {
|
||||
return post("/api/data-management/datasets", data);
|
||||
}
|
||||
|
||||
// 根据ID获取数据集详情
|
||||
export function queryDatasetByIdUsingGet(id: string | number) {
|
||||
return get(`/api/data-management/datasets/${id}`);
|
||||
}
|
||||
|
||||
// 更新数据集
|
||||
export function updateDatasetByIdUsingPut(id: string | number, data: any) {
|
||||
return put(`/api/data-management/datasets/${id}`, data);
|
||||
}
|
||||
|
||||
// 删除数据集
|
||||
export function deleteDatasetByIdUsingDelete(id: string | number) {
|
||||
return del(`/api/data-management/datasets/${id}`);
|
||||
}
|
||||
|
||||
// 下载数据集
|
||||
export function downloadDatasetUsingGet(id: string | number) {
|
||||
return download(`/api/data-management/datasets/${id}/files/download`);
|
||||
}
|
||||
|
||||
// 验证数据集
|
||||
export function validateDatasetUsingPost(id: string | number, data?: any) {
|
||||
return post(`/api/data-management/datasets/${id}/validate`, data);
|
||||
}
|
||||
|
||||
// 获取数据集文件列表
|
||||
export function queryDatasetFilesUsingGet(id: string | number, params?: any) {
|
||||
return get(`/api/data-management/datasets/${id}/files`, params);
|
||||
}
|
||||
|
||||
// 上传数据集文件
|
||||
export function uploadDatasetFileUsingPost(id: string | number, data: any) {
|
||||
return post(`/api/data-management/datasets/${id}/files`, data);
|
||||
}
|
||||
|
||||
export function downloadFileByIdUsingGet(
|
||||
id: string | number,
|
||||
fileId: string | number,
|
||||
fileName: string
|
||||
) {
|
||||
return download(
|
||||
`/api/data-management/datasets/${id}/files/${fileId}/download`,
|
||||
null,
|
||||
fileName
|
||||
);
|
||||
}
|
||||
|
||||
// 删除数据集文件
|
||||
export function deleteDatasetFileUsingDelete(
|
||||
datasetId: string | number,
|
||||
fileId: string | number
|
||||
) {
|
||||
return del(`/api/data-management/datasets/${datasetId}/files/${fileId}`);
|
||||
}
|
||||
|
||||
// 文件预览
|
||||
export function previewDatasetUsingGet(id: string | number, params?: any) {
|
||||
return get(`/api/data-management/datasets/${id}/preview`, params);
|
||||
}
|
||||
|
||||
// 获取数据集标签
|
||||
export function queryDatasetTagsUsingGet(params?: any) {
|
||||
return get("/api/data-management/tags", params);
|
||||
}
|
||||
|
||||
// 创建数据集标签
|
||||
export function createDatasetTagUsingPost(data: any) {
|
||||
return post("/api/data-management/tags", data);
|
||||
}
|
||||
|
||||
// 更新数据集标签
|
||||
export function updateDatasetTagUsingPut(data: any) {
|
||||
return put(`/api/data-management/tags`, data);
|
||||
}
|
||||
|
||||
// 删除数据集标签
|
||||
export function deleteDatasetTagUsingDelete(data: any) {
|
||||
return del(`/api/data-management/tags`, data);
|
||||
}
|
||||
|
||||
// 数据集质量检查
|
||||
export function checkDatasetQualityUsingPost(id: string | number, data?: any) {
|
||||
return post(`/api/data-management/datasets/${id}/quality-check`, data);
|
||||
}
|
||||
|
||||
// 获取数据集质量报告
|
||||
export function getDatasetQualityReportUsingGet(id: string | number) {
|
||||
return get(`/api/data-management/datasets/${id}/quality-report`);
|
||||
}
|
||||
|
||||
// 数据集分析
|
||||
export function analyzeDatasetUsingPost(id: string | number, data?: any) {
|
||||
return post(`/api/data-management/datasets/${id}/analyze`, data);
|
||||
}
|
||||
|
||||
// 获取数据集分析结果
|
||||
export function getDatasetAnalysisUsingGet(id: string | number) {
|
||||
return get(`/api/data-management/datasets/${id}/analysis`);
|
||||
}
|
||||
|
||||
// 导出数据集
|
||||
export function exportDatasetUsingPost(id: string | number, data: any) {
|
||||
return post(`/api/data-management/datasets/${id}/export`, data);
|
||||
}
|
||||
|
||||
// 复制数据集
|
||||
export function copyDatasetUsingPost(id: string | number, data: any) {
|
||||
return post(`/api/data-management/datasets/${id}/copy`, data);
|
||||
}
|
||||
|
||||
// 获取数据集版本列表
|
||||
export function queryDatasetVersionsUsingGet(
|
||||
id: string | number,
|
||||
params?: any
|
||||
) {
|
||||
return get(`/api/data-management/datasets/${id}/versions`, params);
|
||||
}
|
||||
|
||||
// 创建数据集版本
|
||||
export function createDatasetVersionUsingPost(id: string | number, data: any) {
|
||||
return post(`/api/data-management/datasets/${id}/versions`, data);
|
||||
}
|
||||
|
||||
// 切换数据集版本
|
||||
export function switchDatasetVersionUsingPut(
|
||||
id: string | number,
|
||||
versionId: string | number
|
||||
) {
|
||||
return put(
|
||||
`/api/data-management/datasets/${id}/versions/${versionId}/switch`
|
||||
);
|
||||
}
|
||||
|
||||
// 删除数据集版本
|
||||
export function deleteDatasetVersionUsingDelete(
|
||||
id: string | number,
|
||||
versionId: string | number
|
||||
) {
|
||||
return del(`/api/data-management/datasets/${id}/versions/${versionId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 文件上传相关接口
|
||||
*/
|
||||
|
||||
export function preUploadUsingPost(id: string | number, data: any) {
|
||||
return post(
|
||||
`/api/data-management/datasets/${id}/files/upload/pre-upload`,
|
||||
data
|
||||
);
|
||||
}
|
||||
|
||||
export function cancelUploadUsingPut(id) {
|
||||
return put(
|
||||
`/api/data-management/datasets/upload/cancel-upload/${id}`,
|
||||
{},
|
||||
{ showLoading: false }
|
||||
);
|
||||
}
|
||||
|
||||
export function uploadFileChunkUsingPost(id: string | number, params, config) {
|
||||
return post(
|
||||
`/api/data-management/datasets/${id}/files/upload/chunk`,
|
||||
params,
|
||||
{
|
||||
showLoading: false,
|
||||
...config,
|
||||
}
|
||||
);
|
||||
}
|
||||
import { get, post, put, del, download } from "@/utils/request";
|
||||
|
||||
// 数据集统计接口
|
||||
export function getDatasetStatisticsUsingGet() {
|
||||
return get("/api/data-management/datasets/statistics");
|
||||
}
|
||||
|
||||
export function queryDatasetStatisticsByIdUsingGet(id: string | number) {
|
||||
return get(`/api/data-management/datasets/${id}/statistics`);
|
||||
}
|
||||
|
||||
// 查询数据集列表
|
||||
export function queryDatasetsUsingGet(params?: any) {
|
||||
return get("/api/data-management/datasets", params);
|
||||
}
|
||||
|
||||
// 创建数据集
|
||||
export function createDatasetUsingPost(data: any) {
|
||||
return post("/api/data-management/datasets", data);
|
||||
}
|
||||
|
||||
// 根据ID获取数据集详情
|
||||
export function queryDatasetByIdUsingGet(id: string | number) {
|
||||
return get(`/api/data-management/datasets/${id}`);
|
||||
}
|
||||
|
||||
// 更新数据集
|
||||
export function updateDatasetByIdUsingPut(id: string | number, data: any) {
|
||||
return put(`/api/data-management/datasets/${id}`, data);
|
||||
}
|
||||
|
||||
// 删除数据集
|
||||
export function deleteDatasetByIdUsingDelete(id: string | number) {
|
||||
return del(`/api/data-management/datasets/${id}`);
|
||||
}
|
||||
|
||||
// 下载数据集
|
||||
export function downloadDatasetUsingGet(id: string | number) {
|
||||
return download(`/api/data-management/datasets/${id}/files/download`);
|
||||
}
|
||||
|
||||
// 验证数据集
|
||||
export function validateDatasetUsingPost(id: string | number, data?: any) {
|
||||
return post(`/api/data-management/datasets/${id}/validate`, data);
|
||||
}
|
||||
|
||||
// 获取数据集文件列表
|
||||
export function queryDatasetFilesUsingGet(id: string | number, params?: any) {
|
||||
return get(`/api/data-management/datasets/${id}/files`, params);
|
||||
}
|
||||
|
||||
// 上传数据集文件
|
||||
export function uploadDatasetFileUsingPost(id: string | number, data: any) {
|
||||
return post(`/api/data-management/datasets/${id}/files`, data);
|
||||
}
|
||||
|
||||
export function downloadFileByIdUsingGet(
|
||||
id: string | number,
|
||||
fileId: string | number,
|
||||
fileName: string
|
||||
) {
|
||||
return download(
|
||||
`/api/data-management/datasets/${id}/files/${fileId}/download`,
|
||||
null,
|
||||
fileName
|
||||
);
|
||||
}
|
||||
|
||||
// 删除数据集文件
|
||||
export function deleteDatasetFileUsingDelete(
|
||||
datasetId: string | number,
|
||||
fileId: string | number
|
||||
) {
|
||||
return del(`/api/data-management/datasets/${datasetId}/files/${fileId}`);
|
||||
}
|
||||
|
||||
// 文件预览
|
||||
export function previewDatasetUsingGet(id: string | number, params?: any) {
|
||||
return get(`/api/data-management/datasets/${id}/preview`, params);
|
||||
}
|
||||
|
||||
// 获取数据集标签
|
||||
export function queryDatasetTagsUsingGet(params?: any) {
|
||||
return get("/api/data-management/tags", params);
|
||||
}
|
||||
|
||||
// 创建数据集标签
|
||||
export function createDatasetTagUsingPost(data: any) {
|
||||
return post("/api/data-management/tags", data);
|
||||
}
|
||||
|
||||
// 更新数据集标签
|
||||
export function updateDatasetTagUsingPut(data: any) {
|
||||
return put(`/api/data-management/tags`, data);
|
||||
}
|
||||
|
||||
// 删除数据集标签
|
||||
export function deleteDatasetTagUsingDelete(data: any) {
|
||||
return del(`/api/data-management/tags`, data);
|
||||
}
|
||||
|
||||
// 数据集质量检查
|
||||
export function checkDatasetQualityUsingPost(id: string | number, data?: any) {
|
||||
return post(`/api/data-management/datasets/${id}/quality-check`, data);
|
||||
}
|
||||
|
||||
// 获取数据集质量报告
|
||||
export function getDatasetQualityReportUsingGet(id: string | number) {
|
||||
return get(`/api/data-management/datasets/${id}/quality-report`);
|
||||
}
|
||||
|
||||
// 数据集分析
|
||||
export function analyzeDatasetUsingPost(id: string | number, data?: any) {
|
||||
return post(`/api/data-management/datasets/${id}/analyze`, data);
|
||||
}
|
||||
|
||||
// 获取数据集分析结果
|
||||
export function getDatasetAnalysisUsingGet(id: string | number) {
|
||||
return get(`/api/data-management/datasets/${id}/analysis`);
|
||||
}
|
||||
|
||||
// 导出数据集
|
||||
export function exportDatasetUsingPost(id: string | number, data: any) {
|
||||
return post(`/api/data-management/datasets/${id}/export`, data);
|
||||
}
|
||||
|
||||
// 复制数据集
|
||||
export function copyDatasetUsingPost(id: string | number, data: any) {
|
||||
return post(`/api/data-management/datasets/${id}/copy`, data);
|
||||
}
|
||||
|
||||
// 获取数据集版本列表
|
||||
export function queryDatasetVersionsUsingGet(
|
||||
id: string | number,
|
||||
params?: any
|
||||
) {
|
||||
return get(`/api/data-management/datasets/${id}/versions`, params);
|
||||
}
|
||||
|
||||
// 创建数据集版本
|
||||
export function createDatasetVersionUsingPost(id: string | number, data: any) {
|
||||
return post(`/api/data-management/datasets/${id}/versions`, data);
|
||||
}
|
||||
|
||||
// 切换数据集版本
|
||||
export function switchDatasetVersionUsingPut(
|
||||
id: string | number,
|
||||
versionId: string | number
|
||||
) {
|
||||
return put(
|
||||
`/api/data-management/datasets/${id}/versions/${versionId}/switch`
|
||||
);
|
||||
}
|
||||
|
||||
// 删除数据集版本
|
||||
export function deleteDatasetVersionUsingDelete(
|
||||
id: string | number,
|
||||
versionId: string | number
|
||||
) {
|
||||
return del(`/api/data-management/datasets/${id}/versions/${versionId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 文件上传相关接口
|
||||
*/
|
||||
|
||||
export function preUploadUsingPost(id: string | number, data: any) {
|
||||
return post(
|
||||
`/api/data-management/datasets/${id}/files/upload/pre-upload`,
|
||||
data
|
||||
);
|
||||
}
|
||||
|
||||
export function cancelUploadUsingPut(id) {
|
||||
return put(
|
||||
`/api/data-management/datasets/upload/cancel-upload/${id}`,
|
||||
{},
|
||||
{ showLoading: false }
|
||||
);
|
||||
}
|
||||
|
||||
export function uploadFileChunkUsingPost(id: string | number, params, config) {
|
||||
return post(
|
||||
`/api/data-management/datasets/${id}/files/upload/chunk`,
|
||||
params,
|
||||
{
|
||||
showLoading: false,
|
||||
...config,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,223 +1,223 @@
|
||||
import {
|
||||
DatasetType,
|
||||
DatasetStatus,
|
||||
type Dataset,
|
||||
DatasetSubType,
|
||||
DataSource,
|
||||
} from "@/pages/DataManagement/dataset.model";
|
||||
import { formatBytes, formatDateTime } from "@/utils/unit";
|
||||
import {
|
||||
CheckCircleOutlined,
|
||||
ClockCircleOutlined,
|
||||
CloseCircleOutlined,
|
||||
FileOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { AnyObject } from "antd/es/_util/type";
|
||||
import {
|
||||
FileImage,
|
||||
FileText,
|
||||
Video,
|
||||
FileCode,
|
||||
MessageCircleMore,
|
||||
ImagePlus,
|
||||
FileMusic,
|
||||
Music,
|
||||
Videotape,
|
||||
Database,
|
||||
Image,
|
||||
ScanText,
|
||||
} from "lucide-react";
|
||||
|
||||
export const datasetTypeMap: Record<
|
||||
string,
|
||||
{
|
||||
value: DatasetType;
|
||||
label: string;
|
||||
order: number;
|
||||
description: string;
|
||||
icon?: any;
|
||||
iconColor?: string;
|
||||
children: DatasetSubType[];
|
||||
}
|
||||
> = {
|
||||
[DatasetType.TEXT]: {
|
||||
value: DatasetType.TEXT,
|
||||
label: "文本",
|
||||
order: 1,
|
||||
icon: ScanText,
|
||||
iconColor: "blue",
|
||||
children: [
|
||||
DatasetSubType.TEXT_DOCUMENT,
|
||||
DatasetSubType.TEXT_WEB,
|
||||
DatasetSubType.TEXT_DIALOG,
|
||||
],
|
||||
description: "用于处理和分析文本数据的数据集",
|
||||
},
|
||||
[DatasetType.IMAGE]: {
|
||||
value: DatasetType.IMAGE,
|
||||
label: "图像",
|
||||
order: 2,
|
||||
icon: Image,
|
||||
iconColor: "green",
|
||||
children: [DatasetSubType.IMAGE_IMAGE, DatasetSubType.IMAGE_CAPTION],
|
||||
description: "用于处理和分析图像数据的数据集",
|
||||
},
|
||||
[DatasetType.AUDIO]: {
|
||||
value: DatasetType.AUDIO,
|
||||
label: "音频",
|
||||
order: 3,
|
||||
icon: Music,
|
||||
iconColor: "orange",
|
||||
children: [DatasetSubType.AUDIO_AUDIO, DatasetSubType.AUDIO_JSONL],
|
||||
description: "用于处理和分析音频数据的数据集",
|
||||
},
|
||||
[DatasetType.VIDEO]: {
|
||||
value: DatasetType.VIDEO,
|
||||
label: "视频",
|
||||
order: 3,
|
||||
icon: Video,
|
||||
iconColor: "purple",
|
||||
children: [DatasetSubType.VIDEO_VIDEO, DatasetSubType.VIDEO_JSONL],
|
||||
description: "用于处理和分析视频数据的数据集",
|
||||
},
|
||||
};
|
||||
|
||||
export const datasetSubTypeMap: Record<
|
||||
string,
|
||||
{
|
||||
value: DatasetSubType;
|
||||
label: string;
|
||||
order?: number;
|
||||
description?: string;
|
||||
icon?: any;
|
||||
color?: string;
|
||||
}
|
||||
> = {
|
||||
[DatasetSubType.TEXT_DOCUMENT]: {
|
||||
value: DatasetSubType.TEXT_DOCUMENT,
|
||||
label: "文档",
|
||||
color: "blue",
|
||||
icon: FileText,
|
||||
description: "用于存储和处理各种文档格式的文本数据集",
|
||||
},
|
||||
[DatasetSubType.TEXT_WEB]: {
|
||||
value: DatasetSubType.TEXT_WEB,
|
||||
label: "网页",
|
||||
color: "cyan",
|
||||
icon: FileCode,
|
||||
description: "用于存储和处理网页数据集",
|
||||
},
|
||||
[DatasetSubType.TEXT_DIALOG]: {
|
||||
value: DatasetSubType.TEXT_DIALOG,
|
||||
label: "对话",
|
||||
color: "teal",
|
||||
icon: MessageCircleMore,
|
||||
description: "用于存储和处理对话数据的数据集",
|
||||
},
|
||||
[DatasetSubType.IMAGE_IMAGE]: {
|
||||
value: DatasetSubType.IMAGE_IMAGE,
|
||||
label: "图像",
|
||||
color: "green",
|
||||
icon: FileImage,
|
||||
description: "用于大规模图像预训练模型的数据集",
|
||||
},
|
||||
[DatasetSubType.IMAGE_CAPTION]: {
|
||||
value: DatasetSubType.IMAGE_CAPTION,
|
||||
label: "图像+caption",
|
||||
color: "lightgreen",
|
||||
icon: ImagePlus,
|
||||
description: "用于图像标题生成的数据集",
|
||||
},
|
||||
[DatasetSubType.AUDIO_AUDIO]: {
|
||||
value: DatasetSubType.AUDIO_AUDIO,
|
||||
label: "音频",
|
||||
color: "purple",
|
||||
icon: Music,
|
||||
description: "用于大规模音频预训练模型的数据集",
|
||||
},
|
||||
[DatasetSubType.AUDIO_JSONL]: {
|
||||
value: DatasetSubType.AUDIO_JSONL,
|
||||
label: "音频+JSONL",
|
||||
color: "purple",
|
||||
icon: FileMusic,
|
||||
description: "用于大规模音频预训练模型的数据集",
|
||||
},
|
||||
[DatasetSubType.VIDEO_VIDEO]: {
|
||||
value: DatasetSubType.VIDEO_VIDEO,
|
||||
label: "视频",
|
||||
color: "orange",
|
||||
icon: Video,
|
||||
description: "用于大规模视频预训练模型的数据集",
|
||||
},
|
||||
[DatasetSubType.VIDEO_JSONL]: {
|
||||
value: DatasetSubType.VIDEO_JSONL,
|
||||
label: "视频+JSONL",
|
||||
color: "orange",
|
||||
icon: Videotape,
|
||||
description: "用于大规模视频预训练模型的数据集",
|
||||
},
|
||||
};
|
||||
|
||||
export const datasetStatusMap = {
|
||||
[DatasetStatus.ACTIVE]: {
|
||||
label: "活跃",
|
||||
value: DatasetStatus.ACTIVE,
|
||||
color: "#409f17ff",
|
||||
icon: <CheckCircleOutlined />,
|
||||
},
|
||||
[DatasetStatus.PROCESSING]: {
|
||||
label: "处理中",
|
||||
value: DatasetStatus.PROCESSING,
|
||||
color: "#2673e5",
|
||||
icon: <ClockCircleOutlined />,
|
||||
},
|
||||
[DatasetStatus.INACTIVE]: {
|
||||
label: "未激活",
|
||||
value: DatasetStatus.INACTIVE,
|
||||
color: "#4f4444ff",
|
||||
icon: <CloseCircleOutlined />,
|
||||
},
|
||||
[DatasetStatus.DRAFT]: {
|
||||
label: "草稿",
|
||||
value: DatasetStatus.DRAFT,
|
||||
color: "#a1a1a1ff",
|
||||
icon: <FileOutlined />,
|
||||
},
|
||||
};
|
||||
|
||||
export const dataSourceMap: Record<string, { label: string; value: string }> = {
|
||||
[DataSource.UPLOAD]: { label: "本地上传", value: DataSource.UPLOAD },
|
||||
[DataSource.COLLECTION]: { label: "归集任务导入 ", value: DataSource.COLLECTION },
|
||||
// [DataSource.DATABASE]: { label: "数据库导入", value: DataSource.DATABASE },
|
||||
// [DataSource.NAS]: { label: "NAS导入", value: DataSource.NAS },
|
||||
// [DataSource.OBS]: { label: "OBS导入", value: DataSource.OBS },
|
||||
};
|
||||
|
||||
export const dataSourceOptions = Object.values(dataSourceMap);
|
||||
|
||||
export function mapDataset(dataset: AnyObject): Dataset {
|
||||
const { icon: IconComponent, iconColor } =
|
||||
datasetTypeMap[dataset?.datasetType] || {};
|
||||
return {
|
||||
...dataset,
|
||||
key: dataset.id,
|
||||
type: datasetTypeMap[dataset.datasetType]?.label || "未知",
|
||||
size: formatBytes(dataset.totalSize || 0),
|
||||
createdAt: formatDateTime(dataset.createdAt) || "--",
|
||||
updatedAt: formatDateTime(dataset?.updatedAt) || "--",
|
||||
icon: IconComponent ? <IconComponent className="w-full h-full" /> : <Database />,
|
||||
status: datasetStatusMap[dataset.status],
|
||||
statistics: [
|
||||
{ label: "文件数", value: dataset.fileCount || 0 },
|
||||
{ label: "大小", value: formatBytes(dataset.totalSize || 0) },
|
||||
],
|
||||
lastModified: dataset.updatedAt,
|
||||
};
|
||||
}
|
||||
|
||||
export const datasetTypes = Object.values(datasetTypeMap).map((type) => ({
|
||||
...type,
|
||||
options: type.children?.map(
|
||||
(subType) => datasetSubTypeMap[subType as keyof typeof datasetSubTypeMap]
|
||||
),
|
||||
}));
|
||||
import {
|
||||
DatasetType,
|
||||
DatasetStatus,
|
||||
type Dataset,
|
||||
DatasetSubType,
|
||||
DataSource,
|
||||
} from "@/pages/DataManagement/dataset.model";
|
||||
import { formatBytes, formatDateTime } from "@/utils/unit";
|
||||
import {
|
||||
CheckCircleOutlined,
|
||||
ClockCircleOutlined,
|
||||
CloseCircleOutlined,
|
||||
FileOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { AnyObject } from "antd/es/_util/type";
|
||||
import {
|
||||
FileImage,
|
||||
FileText,
|
||||
Video,
|
||||
FileCode,
|
||||
MessageCircleMore,
|
||||
ImagePlus,
|
||||
FileMusic,
|
||||
Music,
|
||||
Videotape,
|
||||
Database,
|
||||
Image,
|
||||
ScanText,
|
||||
} from "lucide-react";
|
||||
|
||||
export const datasetTypeMap: Record<
|
||||
string,
|
||||
{
|
||||
value: DatasetType;
|
||||
label: string;
|
||||
order: number;
|
||||
description: string;
|
||||
icon?: any;
|
||||
iconColor?: string;
|
||||
children: DatasetSubType[];
|
||||
}
|
||||
> = {
|
||||
[DatasetType.TEXT]: {
|
||||
value: DatasetType.TEXT,
|
||||
label: "文本",
|
||||
order: 1,
|
||||
icon: ScanText,
|
||||
iconColor: "blue",
|
||||
children: [
|
||||
DatasetSubType.TEXT_DOCUMENT,
|
||||
DatasetSubType.TEXT_WEB,
|
||||
DatasetSubType.TEXT_DIALOG,
|
||||
],
|
||||
description: "用于处理和分析文本数据的数据集",
|
||||
},
|
||||
[DatasetType.IMAGE]: {
|
||||
value: DatasetType.IMAGE,
|
||||
label: "图像",
|
||||
order: 2,
|
||||
icon: Image,
|
||||
iconColor: "green",
|
||||
children: [DatasetSubType.IMAGE_IMAGE, DatasetSubType.IMAGE_CAPTION],
|
||||
description: "用于处理和分析图像数据的数据集",
|
||||
},
|
||||
[DatasetType.AUDIO]: {
|
||||
value: DatasetType.AUDIO,
|
||||
label: "音频",
|
||||
order: 3,
|
||||
icon: Music,
|
||||
iconColor: "orange",
|
||||
children: [DatasetSubType.AUDIO_AUDIO, DatasetSubType.AUDIO_JSONL],
|
||||
description: "用于处理和分析音频数据的数据集",
|
||||
},
|
||||
[DatasetType.VIDEO]: {
|
||||
value: DatasetType.VIDEO,
|
||||
label: "视频",
|
||||
order: 3,
|
||||
icon: Video,
|
||||
iconColor: "purple",
|
||||
children: [DatasetSubType.VIDEO_VIDEO, DatasetSubType.VIDEO_JSONL],
|
||||
description: "用于处理和分析视频数据的数据集",
|
||||
},
|
||||
};
|
||||
|
||||
export const datasetSubTypeMap: Record<
|
||||
string,
|
||||
{
|
||||
value: DatasetSubType;
|
||||
label: string;
|
||||
order?: number;
|
||||
description?: string;
|
||||
icon?: any;
|
||||
color?: string;
|
||||
}
|
||||
> = {
|
||||
[DatasetSubType.TEXT_DOCUMENT]: {
|
||||
value: DatasetSubType.TEXT_DOCUMENT,
|
||||
label: "文档",
|
||||
color: "blue",
|
||||
icon: FileText,
|
||||
description: "用于存储和处理各种文档格式的文本数据集",
|
||||
},
|
||||
[DatasetSubType.TEXT_WEB]: {
|
||||
value: DatasetSubType.TEXT_WEB,
|
||||
label: "网页",
|
||||
color: "cyan",
|
||||
icon: FileCode,
|
||||
description: "用于存储和处理网页数据集",
|
||||
},
|
||||
[DatasetSubType.TEXT_DIALOG]: {
|
||||
value: DatasetSubType.TEXT_DIALOG,
|
||||
label: "对话",
|
||||
color: "teal",
|
||||
icon: MessageCircleMore,
|
||||
description: "用于存储和处理对话数据的数据集",
|
||||
},
|
||||
[DatasetSubType.IMAGE_IMAGE]: {
|
||||
value: DatasetSubType.IMAGE_IMAGE,
|
||||
label: "图像",
|
||||
color: "green",
|
||||
icon: FileImage,
|
||||
description: "用于大规模图像预训练模型的数据集",
|
||||
},
|
||||
[DatasetSubType.IMAGE_CAPTION]: {
|
||||
value: DatasetSubType.IMAGE_CAPTION,
|
||||
label: "图像+caption",
|
||||
color: "lightgreen",
|
||||
icon: ImagePlus,
|
||||
description: "用于图像标题生成的数据集",
|
||||
},
|
||||
[DatasetSubType.AUDIO_AUDIO]: {
|
||||
value: DatasetSubType.AUDIO_AUDIO,
|
||||
label: "音频",
|
||||
color: "purple",
|
||||
icon: Music,
|
||||
description: "用于大规模音频预训练模型的数据集",
|
||||
},
|
||||
[DatasetSubType.AUDIO_JSONL]: {
|
||||
value: DatasetSubType.AUDIO_JSONL,
|
||||
label: "音频+JSONL",
|
||||
color: "purple",
|
||||
icon: FileMusic,
|
||||
description: "用于大规模音频预训练模型的数据集",
|
||||
},
|
||||
[DatasetSubType.VIDEO_VIDEO]: {
|
||||
value: DatasetSubType.VIDEO_VIDEO,
|
||||
label: "视频",
|
||||
color: "orange",
|
||||
icon: Video,
|
||||
description: "用于大规模视频预训练模型的数据集",
|
||||
},
|
||||
[DatasetSubType.VIDEO_JSONL]: {
|
||||
value: DatasetSubType.VIDEO_JSONL,
|
||||
label: "视频+JSONL",
|
||||
color: "orange",
|
||||
icon: Videotape,
|
||||
description: "用于大规模视频预训练模型的数据集",
|
||||
},
|
||||
};
|
||||
|
||||
export const datasetStatusMap = {
|
||||
[DatasetStatus.ACTIVE]: {
|
||||
label: "活跃",
|
||||
value: DatasetStatus.ACTIVE,
|
||||
color: "#409f17ff",
|
||||
icon: <CheckCircleOutlined />,
|
||||
},
|
||||
[DatasetStatus.PROCESSING]: {
|
||||
label: "处理中",
|
||||
value: DatasetStatus.PROCESSING,
|
||||
color: "#2673e5",
|
||||
icon: <ClockCircleOutlined />,
|
||||
},
|
||||
[DatasetStatus.INACTIVE]: {
|
||||
label: "未激活",
|
||||
value: DatasetStatus.INACTIVE,
|
||||
color: "#4f4444ff",
|
||||
icon: <CloseCircleOutlined />,
|
||||
},
|
||||
[DatasetStatus.DRAFT]: {
|
||||
label: "草稿",
|
||||
value: DatasetStatus.DRAFT,
|
||||
color: "#a1a1a1ff",
|
||||
icon: <FileOutlined />,
|
||||
},
|
||||
};
|
||||
|
||||
export const dataSourceMap: Record<string, { label: string; value: string }> = {
|
||||
[DataSource.UPLOAD]: { label: "本地上传", value: DataSource.UPLOAD },
|
||||
[DataSource.COLLECTION]: { label: "归集任务导入 ", value: DataSource.COLLECTION },
|
||||
// [DataSource.DATABASE]: { label: "数据库导入", value: DataSource.DATABASE },
|
||||
// [DataSource.NAS]: { label: "NAS导入", value: DataSource.NAS },
|
||||
// [DataSource.OBS]: { label: "OBS导入", value: DataSource.OBS },
|
||||
};
|
||||
|
||||
export const dataSourceOptions = Object.values(dataSourceMap);
|
||||
|
||||
export function mapDataset(dataset: AnyObject): Dataset {
|
||||
const { icon: IconComponent, iconColor } =
|
||||
datasetTypeMap[dataset?.datasetType] || {};
|
||||
return {
|
||||
...dataset,
|
||||
key: dataset.id,
|
||||
type: datasetTypeMap[dataset.datasetType]?.label || "未知",
|
||||
size: formatBytes(dataset.totalSize || 0),
|
||||
createdAt: formatDateTime(dataset.createdAt) || "--",
|
||||
updatedAt: formatDateTime(dataset?.updatedAt) || "--",
|
||||
icon: IconComponent ? <IconComponent className="w-full h-full" /> : <Database />,
|
||||
status: datasetStatusMap[dataset.status],
|
||||
statistics: [
|
||||
{ label: "文件数", value: dataset.fileCount || 0 },
|
||||
{ label: "大小", value: formatBytes(dataset.totalSize || 0) },
|
||||
],
|
||||
lastModified: dataset.updatedAt,
|
||||
};
|
||||
}
|
||||
|
||||
export const datasetTypes = Object.values(datasetTypeMap).map((type) => ({
|
||||
...type,
|
||||
options: type.children?.map(
|
||||
(subType) => datasetSubTypeMap[subType as keyof typeof datasetSubTypeMap]
|
||||
),
|
||||
}));
|
||||
|
||||
@@ -1,105 +1,105 @@
|
||||
export enum DatasetType {
|
||||
TEXT = "TEXT",
|
||||
IMAGE = "IMAGE",
|
||||
AUDIO = "AUDIO",
|
||||
VIDEO = "VIDEO",
|
||||
}
|
||||
|
||||
export enum DatasetSubType {
|
||||
TEXT_DOCUMENT = "TEXT_DOCUMENT",
|
||||
TEXT_WEB = "TEXT_WEB",
|
||||
TEXT_DIALOG = "TEXT_DIALOG",
|
||||
IMAGE_IMAGE = "IMAGE_IMAGE",
|
||||
IMAGE_CAPTION = "IMAGE_CAPTION",
|
||||
AUDIO_AUDIO = "AUDIO_AUDIO",
|
||||
AUDIO_JSONL = "AUDIO_JSONL",
|
||||
VIDEO_VIDEO = "VIDEO_VIDEO",
|
||||
VIDEO_JSONL = "VIDEO_JSONL",
|
||||
}
|
||||
|
||||
export enum DatasetStatus {
|
||||
ACTIVE = "ACTIVE",
|
||||
INACTIVE = "INACTIVE",
|
||||
PROCESSING = "PROCESSING",
|
||||
DRAFT = "DRAFT",
|
||||
}
|
||||
|
||||
export enum DataSource {
|
||||
UPLOAD = "UPLOAD",
|
||||
COLLECTION = "COLLECTION",
|
||||
DATABASE = "DATABASE",
|
||||
NAS = "NAS",
|
||||
OBS = "OBS",
|
||||
}
|
||||
|
||||
export interface DatasetFile {
|
||||
id: number;
|
||||
fileName: string;
|
||||
size: string;
|
||||
uploadDate: string;
|
||||
path: string;
|
||||
}
|
||||
|
||||
export interface Dataset {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
parentId?: number;
|
||||
datasetType: DatasetType;
|
||||
status: DatasetStatus;
|
||||
size?: string;
|
||||
itemCount?: number;
|
||||
fileCount?: number;
|
||||
createdBy: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
tags: string[];
|
||||
targetLocation?: string;
|
||||
distribution?: Record<string, Record<string, number>>;
|
||||
}
|
||||
|
||||
export interface TagItem {
|
||||
id: string;
|
||||
name: string;
|
||||
color: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface ScheduleConfig {
|
||||
type: "immediate" | "scheduled";
|
||||
scheduleType?: "daily" | "weekly" | "monthly" | "custom";
|
||||
time?: string;
|
||||
dayOfWeek?: string;
|
||||
dayOfMonth?: string;
|
||||
cronExpression?: string;
|
||||
maxExecutions?: number;
|
||||
executionCount?: number;
|
||||
}
|
||||
|
||||
export interface DatasetTask {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
type: string;
|
||||
status: "importing" | "waiting" | "completed" | "failed";
|
||||
progress: number;
|
||||
createdAt: string;
|
||||
importConfig: any;
|
||||
scheduleConfig: ScheduleConfig;
|
||||
nextExecution?: string;
|
||||
lastExecution?: string;
|
||||
executionHistory?: { time: string; status: string }[];
|
||||
}
|
||||
|
||||
export interface TaskItem {
|
||||
key: string;
|
||||
title: string;
|
||||
percent: number;
|
||||
reqId: number;
|
||||
isCancel?: boolean;
|
||||
controller: AbortController;
|
||||
cancelFn?: () => void;
|
||||
updateEvent?: string;
|
||||
size?: number;
|
||||
hasArchive?: boolean;
|
||||
}
|
||||
export enum DatasetType {
|
||||
TEXT = "TEXT",
|
||||
IMAGE = "IMAGE",
|
||||
AUDIO = "AUDIO",
|
||||
VIDEO = "VIDEO",
|
||||
}
|
||||
|
||||
export enum DatasetSubType {
|
||||
TEXT_DOCUMENT = "TEXT_DOCUMENT",
|
||||
TEXT_WEB = "TEXT_WEB",
|
||||
TEXT_DIALOG = "TEXT_DIALOG",
|
||||
IMAGE_IMAGE = "IMAGE_IMAGE",
|
||||
IMAGE_CAPTION = "IMAGE_CAPTION",
|
||||
AUDIO_AUDIO = "AUDIO_AUDIO",
|
||||
AUDIO_JSONL = "AUDIO_JSONL",
|
||||
VIDEO_VIDEO = "VIDEO_VIDEO",
|
||||
VIDEO_JSONL = "VIDEO_JSONL",
|
||||
}
|
||||
|
||||
export enum DatasetStatus {
|
||||
ACTIVE = "ACTIVE",
|
||||
INACTIVE = "INACTIVE",
|
||||
PROCESSING = "PROCESSING",
|
||||
DRAFT = "DRAFT",
|
||||
}
|
||||
|
||||
export enum DataSource {
|
||||
UPLOAD = "UPLOAD",
|
||||
COLLECTION = "COLLECTION",
|
||||
DATABASE = "DATABASE",
|
||||
NAS = "NAS",
|
||||
OBS = "OBS",
|
||||
}
|
||||
|
||||
export interface DatasetFile {
|
||||
id: number;
|
||||
fileName: string;
|
||||
size: string;
|
||||
uploadDate: string;
|
||||
path: string;
|
||||
}
|
||||
|
||||
export interface Dataset {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
parentId?: number;
|
||||
datasetType: DatasetType;
|
||||
status: DatasetStatus;
|
||||
size?: string;
|
||||
itemCount?: number;
|
||||
fileCount?: number;
|
||||
createdBy: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
tags: string[];
|
||||
targetLocation?: string;
|
||||
distribution?: Record<string, Record<string, number>>;
|
||||
}
|
||||
|
||||
export interface TagItem {
|
||||
id: string;
|
||||
name: string;
|
||||
color: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface ScheduleConfig {
|
||||
type: "immediate" | "scheduled";
|
||||
scheduleType?: "daily" | "weekly" | "monthly" | "custom";
|
||||
time?: string;
|
||||
dayOfWeek?: string;
|
||||
dayOfMonth?: string;
|
||||
cronExpression?: string;
|
||||
maxExecutions?: number;
|
||||
executionCount?: number;
|
||||
}
|
||||
|
||||
export interface DatasetTask {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
type: string;
|
||||
status: "importing" | "waiting" | "completed" | "failed";
|
||||
progress: number;
|
||||
createdAt: string;
|
||||
importConfig: any;
|
||||
scheduleConfig: ScheduleConfig;
|
||||
nextExecution?: string;
|
||||
lastExecution?: string;
|
||||
executionHistory?: { time: string; status: string }[];
|
||||
}
|
||||
|
||||
export interface TaskItem {
|
||||
key: string;
|
||||
title: string;
|
||||
percent: number;
|
||||
reqId: number;
|
||||
isCancel?: boolean;
|
||||
controller: AbortController;
|
||||
cancelFn?: () => void;
|
||||
updateEvent?: string;
|
||||
size?: number;
|
||||
hasArchive?: boolean;
|
||||
}
|
||||
|
||||
@@ -1,362 +1,362 @@
|
||||
import {
|
||||
FolderOpen,
|
||||
Settings,
|
||||
ArrowRight,
|
||||
Sparkles,
|
||||
Target,
|
||||
Zap,
|
||||
Database,
|
||||
MessageSquare,
|
||||
GitBranch,
|
||||
} from "lucide-react";
|
||||
import { features, menuItems } from "../Layout/menu";
|
||||
import { useState } from 'react';
|
||||
import { useNavigate } from "react-router";
|
||||
import { Card } from "antd";
|
||||
|
||||
export default function WelcomePage() {
|
||||
const navigate = useNavigate();
|
||||
const [isChecking, setIsChecking] = useState(false);
|
||||
|
||||
// 检查接口连通性的函数
|
||||
const checkDeerFlowDeploy = async (): Promise<boolean> => {
|
||||
try {
|
||||
const response = await fetch('/deer-flow-backend/config', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
timeout: 5000, // 5秒超时
|
||||
cache: 'no-store'
|
||||
});
|
||||
|
||||
// 检查 HTTP 状态码在 200-299 范围内
|
||||
if (response.ok) {
|
||||
return true;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('接口检查失败:', error);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const handleChatClick = async () => {
|
||||
if (isChecking) return; // 防止重复点击
|
||||
|
||||
setIsChecking(true);
|
||||
|
||||
try {
|
||||
const isDeerFlowDeploy = await checkDeerFlowDeploy();
|
||||
|
||||
if (isDeerFlowDeploy) {
|
||||
// 接口正常,执行原有逻辑
|
||||
window.location.href = "/chat";
|
||||
} else {
|
||||
// 接口异常,使用 navigate 跳转
|
||||
navigate("/chat");
|
||||
}
|
||||
} catch (error) {
|
||||
// 发生错误时也使用 navigate 跳转
|
||||
console.error('检查过程中发生错误:', error);
|
||||
navigate("/chat");
|
||||
} finally {
|
||||
setIsChecking(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100">
|
||||
<div className="max-w-7xl mx-auto px-4 py-12">
|
||||
{/* Hero Section */}
|
||||
<div className="text-center mb-16">
|
||||
<div className="inline-flex items-center gap-2 bg-blue-100 text-blue-800 px-4 py-2 rounded-full text-sm font-medium mb-6">
|
||||
<Sparkles className="w-4 h-4" />
|
||||
AI数据集准备工具
|
||||
</div>
|
||||
<h1 className="text-4xl md:text-6xl font-bold text-gray-900 mb-6">
|
||||
构建高质量
|
||||
<span className="text-blue-600"> AI数据集</span>
|
||||
</h1>
|
||||
<p className="text-xl text-gray-600 max-w-3xl mx-auto mb-8">
|
||||
从数据管理到知识生成,一站式解决企业AI数据处理的场景问题。
|
||||
</p>
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-center">
|
||||
<span
|
||||
onClick={() => navigate("/data/management")}
|
||||
className="cursor-pointer rounded px-4 py-2 inline-flex items-center bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-700 hover:to-indigo-700 text-white shadow-lg"
|
||||
>
|
||||
<Database className="mr-2 w-4 h-4" />
|
||||
开始使用
|
||||
</span>
|
||||
<span
|
||||
onClick={handleChatClick}
|
||||
className="cursor-pointer rounded px-4 py-2 inline-flex items-center bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-700 hover:to-pink-700 text-white shadow-lg disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<MessageSquare className="mr-2 w-4 h-4" />
|
||||
{isChecking ? '检查中...' : '对话助手'}
|
||||
</span>
|
||||
<span
|
||||
onClick={() => navigate("/orchestration")}
|
||||
className="cursor-pointer rounded px-4 py-2 inline-flex items-center bg-gradient-to-r from-orange-600 to-amber-600 hover:from-orange-700 hover:to-amber-700 text-white shadow-lg"
|
||||
>
|
||||
数据智能编排
|
||||
<ArrowRight className="ml-2 w-4 h-4" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Features Grid */}
|
||||
<div className="grid md:grid-cols-2 lg:grid-cols-5 gap-6 mb-16">
|
||||
{features.map((feature, index) => (
|
||||
<Card
|
||||
key={index}
|
||||
className="border-0 shadow-lg hover:shadow-xl transition-shadow"
|
||||
>
|
||||
<div className="text-center pb-4">
|
||||
<div className="w-12 h-12 bg-blue-100 rounded-lg flex items-center justify-center mx-auto mb-4">
|
||||
<feature.icon className="w-6 h-6 text-blue-600" />
|
||||
</div>
|
||||
<div className="text-lg">{feature.title}</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<p className="text-gray-600 text-sm">{feature.description}</p>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Menu Items Grid */}
|
||||
<div className="mb-16">
|
||||
<h2 className="text-3xl font-bold text-center text-gray-900 mb-12">
|
||||
功能模块
|
||||
</h2>
|
||||
<div className="grid md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
{menuItems.map((item) => (
|
||||
<Card
|
||||
key={item.id}
|
||||
onClick={() => navigate(item.children ? `/data/${item.children[0].id}`: `/data/${item.id}`)}
|
||||
className="cursor-pointer hover:shadow-lg transition-all duration-200 border-0 shadow-md relative overflow-hidden group"
|
||||
>
|
||||
<div className="text-center relative">
|
||||
<div
|
||||
className={`w-16 h-16 ${item.color} rounded-xl flex items-center justify-center mx-auto mb-4 shadow-lg group-hover:scale-110 transition-transform duration-200`}
|
||||
>
|
||||
<item.icon className="w-8 h-8 text-white" />
|
||||
</div>
|
||||
<div className="flex items-center justify-center gap-2 mb-2"></div>
|
||||
<div className="text-xl group-hover:text-blue-600 transition-colors">
|
||||
{item.title}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="text-sm group-hover:text-gray-700 transition-colors">
|
||||
{item.description}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Data Orchestration Highlight */}
|
||||
<div className="mb-16">
|
||||
<Card className="bg-gradient-to-br from-orange-50 to-amber-50 border-orange-200 shadow-lg">
|
||||
<div className="p-8">
|
||||
<div className="text-center mb-6">
|
||||
<div className="w-16 h-16 bg-gradient-to-br from-orange-500 to-amber-500 rounded-xl flex items-center justify-center mx-auto mb-4">
|
||||
<GitBranch className="w-8 h-8 text-white" />
|
||||
</div>
|
||||
<h3 className="text-2xl font-bold text-orange-900 mb-2">
|
||||
数据智能编排 - 可视化流程设计
|
||||
</h3>
|
||||
<p className="text-orange-700">
|
||||
拖拽式设计复杂数据清洗管道,让数据流转更加直观高效
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-8 mb-6">
|
||||
<div className="space-y-3">
|
||||
<h4 className="font-semibold text-orange-900">
|
||||
🎯 核心功能:
|
||||
</h4>
|
||||
<div className="space-y-2">
|
||||
<div className="bg-white/60 rounded-lg p-3 text-sm text-orange-800">
|
||||
可视化流程设计器
|
||||
</div>
|
||||
<div className="bg-white/60 rounded-lg p-3 text-sm text-orange-800">
|
||||
丰富的数据清洗组件库
|
||||
</div>
|
||||
<div className="bg-white/60 rounded-lg p-3 text-sm text-orange-800">
|
||||
实时流程执行监控
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<h4 className="font-semibold text-orange-900">
|
||||
⚡ 智能特性:
|
||||
</h4>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2 text-sm text-orange-800">
|
||||
<Zap className="w-4 h-4 text-orange-500" />
|
||||
自动优化数据流转路径
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm text-orange-800">
|
||||
<Target className="w-4 h-4 text-orange-500" />
|
||||
智能错误检测和修复建议
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm text-orange-800">
|
||||
<Sparkles className="w-4 h-4 text-orange-500" />
|
||||
模板化流程快速复用
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-center">
|
||||
<span
|
||||
onClick={() => navigate("/orchestration")}
|
||||
className="cursor-pointer rounded px-4 py-2 inline-flex items-center bg-gradient-to-r from-orange-600 to-amber-600 hover:from-orange-700 hover:to-amber-700 text-white shadow-lg"
|
||||
>
|
||||
<GitBranch className="mr-2 w-4 h-4" />
|
||||
开始编排
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Data Agent Highlight */}
|
||||
<div className="mb-16">
|
||||
<Card className="bg-gradient-to-br from-purple-50 to-pink-50 border-purple-200 shadow-lg">
|
||||
<div className="p-8">
|
||||
<div className="text-center mb-6">
|
||||
<div className="w-16 h-16 bg-gradient-to-br from-purple-500 to-pink-500 rounded-xl flex items-center justify-center mx-auto mb-4">
|
||||
<MessageSquare className="w-8 h-8 text-white" />
|
||||
</div>
|
||||
<h3 className="text-2xl font-bold text-purple-900 mb-2">
|
||||
Data Agent - 对话式业务操作
|
||||
</h3>
|
||||
<p className="text-purple-700">
|
||||
告别复杂界面,用自然语言完成所有数据集相关业务
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-8 mb-6">
|
||||
<div className="space-y-3">
|
||||
<h4 className="font-semibold text-purple-900">
|
||||
💬 对话示例:
|
||||
</h4>
|
||||
<div className="space-y-2">
|
||||
<div className="bg-white/60 rounded-lg p-3 text-sm text-purple-800">
|
||||
"帮我创建一个图像分类数据集"
|
||||
</div>
|
||||
<div className="bg-white/60 rounded-lg p-3 text-sm text-purple-800">
|
||||
"分析一下数据质量,生成报告"
|
||||
</div>
|
||||
<div className="bg-white/60 rounded-lg p-3 text-sm text-purple-800">
|
||||
"启动合成任务,目标1000条数据"
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<h4 className="font-semibold text-purple-900">
|
||||
🚀 智能特性:
|
||||
</h4>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2 text-sm text-purple-800">
|
||||
<Zap className="w-4 h-4 text-purple-500" />
|
||||
理解复杂需求,自动执行
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm text-purple-800">
|
||||
<Target className="w-4 h-4 text-purple-500" />
|
||||
提供专业建议和优化方案
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm text-purple-800">
|
||||
<Sparkles className="w-4 h-4 text-purple-500" />
|
||||
学习使用习惯,个性化服务
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-center">
|
||||
<span
|
||||
onClick={handleChatClick}
|
||||
className="cursor-pointer rounded px-4 py-2 inline-flex items-center bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-700 hover:to-pink-700 text-white shadow-lg"
|
||||
>
|
||||
<MessageSquare className="mr-2 w-4 h-4" />
|
||||
{isChecking ? '检查中...' : '开始对话'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Workflow Showcase */}
|
||||
<div className="mb-16">
|
||||
<Card className="bg-gradient-to-br from-blue-50 to-indigo-50 border-blue-200 shadow-lg">
|
||||
<div className="p-8">
|
||||
<div className="text-center mb-8">
|
||||
<h3 className="text-2xl font-bold text-blue-900 mb-2">
|
||||
完整的数据清洗工作流
|
||||
</h3>
|
||||
<p className="text-blue-700">
|
||||
从原始数据到高质量数据集的全流程解决方案
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-4 gap-6 mb-8">
|
||||
<div className="text-center">
|
||||
<div className="w-16 h-16 bg-blue-500 rounded-xl flex items-center justify-center mx-auto mb-4">
|
||||
<FolderOpen className="w-8 h-8 text-white" />
|
||||
</div>
|
||||
<h4 className="font-semibold text-blue-900 mb-2">数据收集</h4>
|
||||
<p className="text-sm text-blue-700">
|
||||
支持多种数据源导入,包括本地文件、数据库、API等
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="w-16 h-16 bg-orange-500 rounded-xl flex items-center justify-center mx-auto mb-4">
|
||||
<GitBranch className="w-8 h-8 text-white" />
|
||||
</div>
|
||||
<h4 className="font-semibold text-blue-900 mb-2">智能编排</h4>
|
||||
<p className="text-sm text-blue-700">
|
||||
可视化设计数据清洗流程,自动化执行复杂任务
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="w-16 h-16 bg-purple-500 rounded-xl flex items-center justify-center mx-auto mb-4">
|
||||
<Settings className="w-8 h-8 text-white" />
|
||||
</div>
|
||||
<h4 className="font-semibold text-blue-900 mb-2">智能处理</h4>
|
||||
<p className="text-sm text-blue-700">
|
||||
自动化的数据清洗、标注和质量评估流程
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="w-16 h-16 bg-green-500 rounded-xl flex items-center justify-center mx-auto mb-4">
|
||||
<Target className="w-8 h-8 text-white" />
|
||||
</div>
|
||||
<h4 className="font-semibold text-blue-900 mb-2">质量保证</h4>
|
||||
<p className="text-sm text-blue-700">
|
||||
全面的质量评估和偏见检测,确保数据集可靠性
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-center">
|
||||
<span
|
||||
onClick={() => navigate("/data/management")}
|
||||
className="cursor-pointer rounded px-4 py-2 inline-flex items-center bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-700 hover:to-pink-700 text-white shadow-lg"
|
||||
>
|
||||
<Sparkles className="mr-2 w-4 h-4" />
|
||||
开始构建数据集
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
import {
|
||||
FolderOpen,
|
||||
Settings,
|
||||
ArrowRight,
|
||||
Sparkles,
|
||||
Target,
|
||||
Zap,
|
||||
Database,
|
||||
MessageSquare,
|
||||
GitBranch,
|
||||
} from "lucide-react";
|
||||
import { features, menuItems } from "../Layout/menu";
|
||||
import { useState } from 'react';
|
||||
import { useNavigate } from "react-router";
|
||||
import { Card } from "antd";
|
||||
|
||||
export default function WelcomePage() {
|
||||
const navigate = useNavigate();
|
||||
const [isChecking, setIsChecking] = useState(false);
|
||||
|
||||
// 检查接口连通性的函数
|
||||
const checkDeerFlowDeploy = async (): Promise<boolean> => {
|
||||
try {
|
||||
const response = await fetch('/deer-flow-backend/config', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
timeout: 5000, // 5秒超时
|
||||
cache: 'no-store'
|
||||
});
|
||||
|
||||
// 检查 HTTP 状态码在 200-299 范围内
|
||||
if (response.ok) {
|
||||
return true;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('接口检查失败:', error);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const handleChatClick = async () => {
|
||||
if (isChecking) return; // 防止重复点击
|
||||
|
||||
setIsChecking(true);
|
||||
|
||||
try {
|
||||
const isDeerFlowDeploy = await checkDeerFlowDeploy();
|
||||
|
||||
if (isDeerFlowDeploy) {
|
||||
// 接口正常,执行原有逻辑
|
||||
window.location.href = "/chat";
|
||||
} else {
|
||||
// 接口异常,使用 navigate 跳转
|
||||
navigate("/chat");
|
||||
}
|
||||
} catch (error) {
|
||||
// 发生错误时也使用 navigate 跳转
|
||||
console.error('检查过程中发生错误:', error);
|
||||
navigate("/chat");
|
||||
} finally {
|
||||
setIsChecking(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100">
|
||||
<div className="max-w-7xl mx-auto px-4 py-12">
|
||||
{/* Hero Section */}
|
||||
<div className="text-center mb-16">
|
||||
<div className="inline-flex items-center gap-2 bg-blue-100 text-blue-800 px-4 py-2 rounded-full text-sm font-medium mb-6">
|
||||
<Sparkles className="w-4 h-4" />
|
||||
AI数据集准备工具
|
||||
</div>
|
||||
<h1 className="text-4xl md:text-6xl font-bold text-gray-900 mb-6">
|
||||
构建高质量
|
||||
<span className="text-blue-600"> AI数据集</span>
|
||||
</h1>
|
||||
<p className="text-xl text-gray-600 max-w-3xl mx-auto mb-8">
|
||||
从数据管理到知识生成,一站式解决企业AI数据处理的场景问题。
|
||||
</p>
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-center">
|
||||
<span
|
||||
onClick={() => navigate("/data/management")}
|
||||
className="cursor-pointer rounded px-4 py-2 inline-flex items-center bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-700 hover:to-indigo-700 text-white shadow-lg"
|
||||
>
|
||||
<Database className="mr-2 w-4 h-4" />
|
||||
开始使用
|
||||
</span>
|
||||
<span
|
||||
onClick={handleChatClick}
|
||||
className="cursor-pointer rounded px-4 py-2 inline-flex items-center bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-700 hover:to-pink-700 text-white shadow-lg disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<MessageSquare className="mr-2 w-4 h-4" />
|
||||
{isChecking ? '检查中...' : '对话助手'}
|
||||
</span>
|
||||
<span
|
||||
onClick={() => navigate("/orchestration")}
|
||||
className="cursor-pointer rounded px-4 py-2 inline-flex items-center bg-gradient-to-r from-orange-600 to-amber-600 hover:from-orange-700 hover:to-amber-700 text-white shadow-lg"
|
||||
>
|
||||
数据智能编排
|
||||
<ArrowRight className="ml-2 w-4 h-4" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Features Grid */}
|
||||
<div className="grid md:grid-cols-2 lg:grid-cols-5 gap-6 mb-16">
|
||||
{features.map((feature, index) => (
|
||||
<Card
|
||||
key={index}
|
||||
className="border-0 shadow-lg hover:shadow-xl transition-shadow"
|
||||
>
|
||||
<div className="text-center pb-4">
|
||||
<div className="w-12 h-12 bg-blue-100 rounded-lg flex items-center justify-center mx-auto mb-4">
|
||||
<feature.icon className="w-6 h-6 text-blue-600" />
|
||||
</div>
|
||||
<div className="text-lg">{feature.title}</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<p className="text-gray-600 text-sm">{feature.description}</p>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Menu Items Grid */}
|
||||
<div className="mb-16">
|
||||
<h2 className="text-3xl font-bold text-center text-gray-900 mb-12">
|
||||
功能模块
|
||||
</h2>
|
||||
<div className="grid md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
{menuItems.map((item) => (
|
||||
<Card
|
||||
key={item.id}
|
||||
onClick={() => navigate(item.children ? `/data/${item.children[0].id}`: `/data/${item.id}`)}
|
||||
className="cursor-pointer hover:shadow-lg transition-all duration-200 border-0 shadow-md relative overflow-hidden group"
|
||||
>
|
||||
<div className="text-center relative">
|
||||
<div
|
||||
className={`w-16 h-16 ${item.color} rounded-xl flex items-center justify-center mx-auto mb-4 shadow-lg group-hover:scale-110 transition-transform duration-200`}
|
||||
>
|
||||
<item.icon className="w-8 h-8 text-white" />
|
||||
</div>
|
||||
<div className="flex items-center justify-center gap-2 mb-2"></div>
|
||||
<div className="text-xl group-hover:text-blue-600 transition-colors">
|
||||
{item.title}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="text-sm group-hover:text-gray-700 transition-colors">
|
||||
{item.description}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Data Orchestration Highlight */}
|
||||
<div className="mb-16">
|
||||
<Card className="bg-gradient-to-br from-orange-50 to-amber-50 border-orange-200 shadow-lg">
|
||||
<div className="p-8">
|
||||
<div className="text-center mb-6">
|
||||
<div className="w-16 h-16 bg-gradient-to-br from-orange-500 to-amber-500 rounded-xl flex items-center justify-center mx-auto mb-4">
|
||||
<GitBranch className="w-8 h-8 text-white" />
|
||||
</div>
|
||||
<h3 className="text-2xl font-bold text-orange-900 mb-2">
|
||||
数据智能编排 - 可视化流程设计
|
||||
</h3>
|
||||
<p className="text-orange-700">
|
||||
拖拽式设计复杂数据清洗管道,让数据流转更加直观高效
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-8 mb-6">
|
||||
<div className="space-y-3">
|
||||
<h4 className="font-semibold text-orange-900">
|
||||
🎯 核心功能:
|
||||
</h4>
|
||||
<div className="space-y-2">
|
||||
<div className="bg-white/60 rounded-lg p-3 text-sm text-orange-800">
|
||||
可视化流程设计器
|
||||
</div>
|
||||
<div className="bg-white/60 rounded-lg p-3 text-sm text-orange-800">
|
||||
丰富的数据清洗组件库
|
||||
</div>
|
||||
<div className="bg-white/60 rounded-lg p-3 text-sm text-orange-800">
|
||||
实时流程执行监控
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<h4 className="font-semibold text-orange-900">
|
||||
⚡ 智能特性:
|
||||
</h4>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2 text-sm text-orange-800">
|
||||
<Zap className="w-4 h-4 text-orange-500" />
|
||||
自动优化数据流转路径
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm text-orange-800">
|
||||
<Target className="w-4 h-4 text-orange-500" />
|
||||
智能错误检测和修复建议
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm text-orange-800">
|
||||
<Sparkles className="w-4 h-4 text-orange-500" />
|
||||
模板化流程快速复用
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-center">
|
||||
<span
|
||||
onClick={() => navigate("/orchestration")}
|
||||
className="cursor-pointer rounded px-4 py-2 inline-flex items-center bg-gradient-to-r from-orange-600 to-amber-600 hover:from-orange-700 hover:to-amber-700 text-white shadow-lg"
|
||||
>
|
||||
<GitBranch className="mr-2 w-4 h-4" />
|
||||
开始编排
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Data Agent Highlight */}
|
||||
<div className="mb-16">
|
||||
<Card className="bg-gradient-to-br from-purple-50 to-pink-50 border-purple-200 shadow-lg">
|
||||
<div className="p-8">
|
||||
<div className="text-center mb-6">
|
||||
<div className="w-16 h-16 bg-gradient-to-br from-purple-500 to-pink-500 rounded-xl flex items-center justify-center mx-auto mb-4">
|
||||
<MessageSquare className="w-8 h-8 text-white" />
|
||||
</div>
|
||||
<h3 className="text-2xl font-bold text-purple-900 mb-2">
|
||||
Data Agent - 对话式业务操作
|
||||
</h3>
|
||||
<p className="text-purple-700">
|
||||
告别复杂界面,用自然语言完成所有数据集相关业务
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-8 mb-6">
|
||||
<div className="space-y-3">
|
||||
<h4 className="font-semibold text-purple-900">
|
||||
💬 对话示例:
|
||||
</h4>
|
||||
<div className="space-y-2">
|
||||
<div className="bg-white/60 rounded-lg p-3 text-sm text-purple-800">
|
||||
"帮我创建一个图像分类数据集"
|
||||
</div>
|
||||
<div className="bg-white/60 rounded-lg p-3 text-sm text-purple-800">
|
||||
"分析一下数据质量,生成报告"
|
||||
</div>
|
||||
<div className="bg-white/60 rounded-lg p-3 text-sm text-purple-800">
|
||||
"启动合成任务,目标1000条数据"
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<h4 className="font-semibold text-purple-900">
|
||||
🚀 智能特性:
|
||||
</h4>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2 text-sm text-purple-800">
|
||||
<Zap className="w-4 h-4 text-purple-500" />
|
||||
理解复杂需求,自动执行
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm text-purple-800">
|
||||
<Target className="w-4 h-4 text-purple-500" />
|
||||
提供专业建议和优化方案
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm text-purple-800">
|
||||
<Sparkles className="w-4 h-4 text-purple-500" />
|
||||
学习使用习惯,个性化服务
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-center">
|
||||
<span
|
||||
onClick={handleChatClick}
|
||||
className="cursor-pointer rounded px-4 py-2 inline-flex items-center bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-700 hover:to-pink-700 text-white shadow-lg"
|
||||
>
|
||||
<MessageSquare className="mr-2 w-4 h-4" />
|
||||
{isChecking ? '检查中...' : '开始对话'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Workflow Showcase */}
|
||||
<div className="mb-16">
|
||||
<Card className="bg-gradient-to-br from-blue-50 to-indigo-50 border-blue-200 shadow-lg">
|
||||
<div className="p-8">
|
||||
<div className="text-center mb-8">
|
||||
<h3 className="text-2xl font-bold text-blue-900 mb-2">
|
||||
完整的数据清洗工作流
|
||||
</h3>
|
||||
<p className="text-blue-700">
|
||||
从原始数据到高质量数据集的全流程解决方案
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-4 gap-6 mb-8">
|
||||
<div className="text-center">
|
||||
<div className="w-16 h-16 bg-blue-500 rounded-xl flex items-center justify-center mx-auto mb-4">
|
||||
<FolderOpen className="w-8 h-8 text-white" />
|
||||
</div>
|
||||
<h4 className="font-semibold text-blue-900 mb-2">数据收集</h4>
|
||||
<p className="text-sm text-blue-700">
|
||||
支持多种数据源导入,包括本地文件、数据库、API等
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="w-16 h-16 bg-orange-500 rounded-xl flex items-center justify-center mx-auto mb-4">
|
||||
<GitBranch className="w-8 h-8 text-white" />
|
||||
</div>
|
||||
<h4 className="font-semibold text-blue-900 mb-2">智能编排</h4>
|
||||
<p className="text-sm text-blue-700">
|
||||
可视化设计数据清洗流程,自动化执行复杂任务
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="w-16 h-16 bg-purple-500 rounded-xl flex items-center justify-center mx-auto mb-4">
|
||||
<Settings className="w-8 h-8 text-white" />
|
||||
</div>
|
||||
<h4 className="font-semibold text-blue-900 mb-2">智能处理</h4>
|
||||
<p className="text-sm text-blue-700">
|
||||
自动化的数据清洗、标注和质量评估流程
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="w-16 h-16 bg-green-500 rounded-xl flex items-center justify-center mx-auto mb-4">
|
||||
<Target className="w-8 h-8 text-white" />
|
||||
</div>
|
||||
<h4 className="font-semibold text-blue-900 mb-2">质量保证</h4>
|
||||
<p className="text-sm text-blue-700">
|
||||
全面的质量评估和偏见检测,确保数据集可靠性
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-center">
|
||||
<span
|
||||
onClick={() => navigate("/data/management")}
|
||||
className="cursor-pointer rounded px-4 py-2 inline-flex items-center bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-700 hover:to-pink-700 text-white shadow-lg"
|
||||
>
|
||||
<Sparkles className="mr-2 w-4 h-4" />
|
||||
开始构建数据集
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,332 +1,332 @@
|
||||
import type React from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Table, Badge, Button, Breadcrumb, Tooltip, App, Card, Input, Empty, Spin } from "antd";
|
||||
import {
|
||||
DeleteOutlined,
|
||||
EditOutlined,
|
||||
ReloadOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { useNavigate, useParams } from "react-router";
|
||||
import DetailHeader from "@/components/DetailHeader";
|
||||
import { SearchControls } from "@/components/SearchControls";
|
||||
import { KBFile, KnowledgeBaseItem } from "../knowledge-base.model";
|
||||
import { mapFileData, mapKnowledgeBase } from "../knowledge-base.const";
|
||||
import {
|
||||
deleteKnowledgeBaseByIdUsingDelete,
|
||||
deleteKnowledgeBaseFileByIdUsingDelete,
|
||||
queryKnowledgeBaseByIdUsingGet,
|
||||
queryKnowledgeBaseFilesUsingGet,
|
||||
retrieveKnowledgeBaseContent,
|
||||
} from "../knowledge-base.api";
|
||||
import useFetchData from "@/hooks/useFetchData";
|
||||
import AddDataDialog from "../components/AddDataDialog";
|
||||
import CreateKnowledgeBase from "../components/CreateKnowledgeBase";
|
||||
|
||||
interface StatisticItem {
|
||||
icon?: React.ReactNode;
|
||||
label: string;
|
||||
value: string | number;
|
||||
}
|
||||
interface RagChunk {
|
||||
id: string;
|
||||
text: string;
|
||||
metadata: string;
|
||||
}
|
||||
interface RecallResult {
|
||||
score: number;
|
||||
entity: RagChunk;
|
||||
id?: string | object;
|
||||
primaryKey?: string;
|
||||
}
|
||||
|
||||
const KnowledgeBaseDetailPage: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const { message } = App.useApp();
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const [knowledgeBase, setKnowledgeBase] = useState<KnowledgeBaseItem | undefined>(undefined);
|
||||
const [showEdit, setShowEdit] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState<'fileList' | 'recallTest'>('fileList');
|
||||
const [recallLoading, setRecallLoading] = useState(false);
|
||||
const [recallResults, setRecallResults] = useState<RecallResult[]>([]);
|
||||
const [recallQuery, setRecallQuery] = useState("");
|
||||
|
||||
const fetchKnowledgeBaseDetails = async (id: string) => {
|
||||
const { data } = await queryKnowledgeBaseByIdUsingGet(id);
|
||||
setKnowledgeBase(mapKnowledgeBase(data));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (id) {
|
||||
fetchKnowledgeBaseDetails(id);
|
||||
}
|
||||
}, [id]);
|
||||
|
||||
const {
|
||||
loading,
|
||||
tableData: files,
|
||||
searchParams,
|
||||
pagination,
|
||||
fetchData: fetchFiles,
|
||||
setSearchParams,
|
||||
handleFiltersChange,
|
||||
handleKeywordChange,
|
||||
} = useFetchData<KBFile>(
|
||||
(params) => id ? queryKnowledgeBaseFilesUsingGet(id, params) : Promise.resolve({ data: [] }),
|
||||
mapFileData
|
||||
);
|
||||
|
||||
// File table logic
|
||||
const handleDeleteFile = async (file: KBFile) => {
|
||||
try {
|
||||
await deleteKnowledgeBaseFileByIdUsingDelete(knowledgeBase!.id, {
|
||||
ids: [file.id]
|
||||
});
|
||||
message.success("文件已删除");
|
||||
fetchFiles();
|
||||
} catch {
|
||||
message.error("文件删除失败");
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteKB = async (kb: KnowledgeBaseItem) => {
|
||||
await deleteKnowledgeBaseByIdUsingDelete(kb.id);
|
||||
message.success("知识库已删除");
|
||||
navigate("/data/knowledge-base");
|
||||
};
|
||||
|
||||
const handleRefreshPage = () => {
|
||||
if (knowledgeBase) {
|
||||
fetchKnowledgeBaseDetails(knowledgeBase.id);
|
||||
}
|
||||
fetchFiles();
|
||||
setShowEdit(false);
|
||||
};
|
||||
|
||||
const handleRecallTest = async () => {
|
||||
if (!recallQuery || !knowledgeBase?.id) return;
|
||||
setRecallLoading(true);
|
||||
try {
|
||||
const result = await retrieveKnowledgeBaseContent({
|
||||
query: recallQuery,
|
||||
topK: 10,
|
||||
threshold: 0.2,
|
||||
knowledgeBaseIds: [knowledgeBase.id],
|
||||
});
|
||||
setRecallResults(result?.data || []);
|
||||
} catch {
|
||||
setRecallResults([]);
|
||||
}
|
||||
setRecallLoading(false);
|
||||
};
|
||||
|
||||
const operations = [
|
||||
{
|
||||
key: "edit",
|
||||
label: "编辑知识库",
|
||||
icon: <EditOutlined className="w-4 h-4" />,
|
||||
onClick: () => {
|
||||
setShowEdit(true);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "refresh",
|
||||
label: "刷新知识库",
|
||||
icon: <ReloadOutlined className="w-4 h-4" />,
|
||||
onClick: () => {
|
||||
handleRefreshPage();
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "delete",
|
||||
label: "删除知识库",
|
||||
danger: true,
|
||||
confirm: {
|
||||
title: "确认删除该知识库吗?",
|
||||
description: "删除后将无法恢复,请谨慎操作。",
|
||||
cancelText: "取消",
|
||||
okText: "删除",
|
||||
okType: "danger",
|
||||
onConfirm: () => knowledgeBase && handleDeleteKB(knowledgeBase),
|
||||
},
|
||||
icon: <DeleteOutlined className="w-4 h-4" />,
|
||||
},
|
||||
];
|
||||
|
||||
const fileOps = [
|
||||
{
|
||||
key: "delete",
|
||||
label: "删除文件",
|
||||
icon: <DeleteOutlined className="w-4 h-4" />,
|
||||
danger: true,
|
||||
onClick: handleDeleteFile,
|
||||
},
|
||||
];
|
||||
|
||||
const fileColumns = [
|
||||
{
|
||||
title: "文件名",
|
||||
dataIndex: "name",
|
||||
key: "name",
|
||||
width: 200,
|
||||
ellipsis: true,
|
||||
fixed: "left" as const,
|
||||
},
|
||||
{
|
||||
title: "状态",
|
||||
dataIndex: "status",
|
||||
key: "vectorizationStatus",
|
||||
width: 120,
|
||||
render: (status: unknown) => {
|
||||
if (typeof status === 'object' && status !== null) {
|
||||
const s = status as { color?: string; label?: string };
|
||||
return <Badge color={s.color} text={s.label} />;
|
||||
}
|
||||
return <Badge color="default" text={String(status)} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "分块数",
|
||||
dataIndex: "chunkCount",
|
||||
key: "chunkCount",
|
||||
width: 100,
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: "创建时间",
|
||||
dataIndex: "createdAt",
|
||||
key: "createdAt",
|
||||
ellipsis: true,
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
title: "更新时间",
|
||||
dataIndex: "updatedAt",
|
||||
key: "updatedAt",
|
||||
ellipsis: true,
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
key: "actions",
|
||||
align: "right" as const,
|
||||
width: 100,
|
||||
render: (_: unknown, file: KBFile) => (
|
||||
<div>
|
||||
{fileOps.map((op) => (
|
||||
<Tooltip key={op.key} title={op.label}>
|
||||
<Button
|
||||
type="text"
|
||||
icon={op.icon}
|
||||
danger={op?.danger}
|
||||
onClick={() => op.onClick(file)}
|
||||
/>
|
||||
</Tooltip>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col">
|
||||
<div className="mb-4">
|
||||
<Breadcrumb>
|
||||
<Breadcrumb.Item>
|
||||
<a onClick={() => navigate("/data/knowledge-base")}>知识库</a>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Item>{knowledgeBase?.name}</Breadcrumb.Item>
|
||||
</Breadcrumb>
|
||||
</div>
|
||||
<DetailHeader
|
||||
data={knowledgeBase}
|
||||
statistics={knowledgeBase && Array.isArray((knowledgeBase as { statistics?: StatisticItem[] }).statistics)
|
||||
? ((knowledgeBase as { statistics?: StatisticItem[] }).statistics ?? [])
|
||||
: []}
|
||||
operations={operations}
|
||||
/>
|
||||
<CreateKnowledgeBase
|
||||
showBtn={false}
|
||||
isEdit={showEdit}
|
||||
data={knowledgeBase}
|
||||
onUpdate={handleRefreshPage}
|
||||
onClose={() => setShowEdit(false)}
|
||||
/>
|
||||
<div className="flex-1 border-card p-6 mt-4">
|
||||
<div className="flex items-center justify-between mb-4 gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Button type={activeTab === 'fileList' ? 'primary' : 'default'} onClick={() => setActiveTab('fileList')}>
|
||||
文件列表
|
||||
</Button>
|
||||
<Button type={activeTab === 'recallTest' ? 'primary' : 'default'} onClick={() => setActiveTab('recallTest')}>
|
||||
召回测试
|
||||
</Button>
|
||||
</div>
|
||||
{activeTab === 'fileList' && (
|
||||
<>
|
||||
<div className="flex-1">
|
||||
<SearchControls
|
||||
searchTerm={searchParams.keyword}
|
||||
onSearchChange={handleKeywordChange}
|
||||
searchPlaceholder="搜索文件名..."
|
||||
filters={[]}
|
||||
onFiltersChange={handleFiltersChange}
|
||||
onClearFilters={() => setSearchParams({ ...searchParams, filter: { type: [], status: [], tags: [] } })}
|
||||
showViewToggle={false}
|
||||
showReload={false}
|
||||
/>
|
||||
</div>
|
||||
<AddDataDialog knowledgeBase={knowledgeBase} onDataAdded={handleRefreshPage} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{activeTab === 'fileList' ? (
|
||||
<Table
|
||||
loading={loading}
|
||||
columns={fileColumns}
|
||||
dataSource={files}
|
||||
rowKey="id"
|
||||
pagination={pagination}
|
||||
scroll={{ y: "calc(100vh - 30rem)" }}
|
||||
/>
|
||||
) : (
|
||||
<div className="p-2">
|
||||
<div style={{ fontSize: 14, fontWeight: 300, marginBottom: 8 }}>基于语义文本检索和全文检索后的加权平均结果</div>
|
||||
<div className="flex items-center mb-4">
|
||||
<Input.Search
|
||||
value={recallQuery}
|
||||
onChange={e => setRecallQuery(e.target.value)}
|
||||
onSearch={handleRecallTest}
|
||||
placeholder="请输入召回测试问题"
|
||||
enterButton="检索"
|
||||
loading={recallLoading}
|
||||
style={{ width: "100%", fontSize: 18, height: 48 }}
|
||||
/>
|
||||
</div>
|
||||
{recallLoading ? (
|
||||
<Spin className="mt-8" />
|
||||
) : recallResults.length === 0 ? (
|
||||
<Empty description="暂无召回结果" />
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{recallResults.map((item, idx) => (
|
||||
<Card key={idx} title={`得分:${item.score?.toFixed(4) ?? "-"}`}
|
||||
extra={<span style={{ fontSize: 12 }}>ID: {item.entity?.id ?? "-"}</span>}
|
||||
style={{ wordBreak: "break-all" }}
|
||||
>
|
||||
<div style={{ marginBottom: 8, fontWeight: 500 }}>{item.entity?.text ?? ""}</div>
|
||||
<div style={{ fontSize: 12, color: '#888' }}>
|
||||
metadata: <pre style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-all', margin: 0 }}>{item.entity?.metadata}</pre>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default KnowledgeBaseDetailPage;
|
||||
import type React from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Table, Badge, Button, Breadcrumb, Tooltip, App, Card, Input, Empty, Spin } from "antd";
|
||||
import {
|
||||
DeleteOutlined,
|
||||
EditOutlined,
|
||||
ReloadOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { useNavigate, useParams } from "react-router";
|
||||
import DetailHeader from "@/components/DetailHeader";
|
||||
import { SearchControls } from "@/components/SearchControls";
|
||||
import { KBFile, KnowledgeBaseItem } from "../knowledge-base.model";
|
||||
import { mapFileData, mapKnowledgeBase } from "../knowledge-base.const";
|
||||
import {
|
||||
deleteKnowledgeBaseByIdUsingDelete,
|
||||
deleteKnowledgeBaseFileByIdUsingDelete,
|
||||
queryKnowledgeBaseByIdUsingGet,
|
||||
queryKnowledgeBaseFilesUsingGet,
|
||||
retrieveKnowledgeBaseContent,
|
||||
} from "../knowledge-base.api";
|
||||
import useFetchData from "@/hooks/useFetchData";
|
||||
import AddDataDialog from "../components/AddDataDialog";
|
||||
import CreateKnowledgeBase from "../components/CreateKnowledgeBase";
|
||||
|
||||
interface StatisticItem {
|
||||
icon?: React.ReactNode;
|
||||
label: string;
|
||||
value: string | number;
|
||||
}
|
||||
interface RagChunk {
|
||||
id: string;
|
||||
text: string;
|
||||
metadata: string;
|
||||
}
|
||||
interface RecallResult {
|
||||
score: number;
|
||||
entity: RagChunk;
|
||||
id?: string | object;
|
||||
primaryKey?: string;
|
||||
}
|
||||
|
||||
const KnowledgeBaseDetailPage: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const { message } = App.useApp();
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const [knowledgeBase, setKnowledgeBase] = useState<KnowledgeBaseItem | undefined>(undefined);
|
||||
const [showEdit, setShowEdit] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState<'fileList' | 'recallTest'>('fileList');
|
||||
const [recallLoading, setRecallLoading] = useState(false);
|
||||
const [recallResults, setRecallResults] = useState<RecallResult[]>([]);
|
||||
const [recallQuery, setRecallQuery] = useState("");
|
||||
|
||||
const fetchKnowledgeBaseDetails = async (id: string) => {
|
||||
const { data } = await queryKnowledgeBaseByIdUsingGet(id);
|
||||
setKnowledgeBase(mapKnowledgeBase(data));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (id) {
|
||||
fetchKnowledgeBaseDetails(id);
|
||||
}
|
||||
}, [id]);
|
||||
|
||||
const {
|
||||
loading,
|
||||
tableData: files,
|
||||
searchParams,
|
||||
pagination,
|
||||
fetchData: fetchFiles,
|
||||
setSearchParams,
|
||||
handleFiltersChange,
|
||||
handleKeywordChange,
|
||||
} = useFetchData<KBFile>(
|
||||
(params) => id ? queryKnowledgeBaseFilesUsingGet(id, params) : Promise.resolve({ data: [] }),
|
||||
mapFileData
|
||||
);
|
||||
|
||||
// File table logic
|
||||
const handleDeleteFile = async (file: KBFile) => {
|
||||
try {
|
||||
await deleteKnowledgeBaseFileByIdUsingDelete(knowledgeBase!.id, {
|
||||
ids: [file.id]
|
||||
});
|
||||
message.success("文件已删除");
|
||||
fetchFiles();
|
||||
} catch {
|
||||
message.error("文件删除失败");
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteKB = async (kb: KnowledgeBaseItem) => {
|
||||
await deleteKnowledgeBaseByIdUsingDelete(kb.id);
|
||||
message.success("知识库已删除");
|
||||
navigate("/data/knowledge-base");
|
||||
};
|
||||
|
||||
const handleRefreshPage = () => {
|
||||
if (knowledgeBase) {
|
||||
fetchKnowledgeBaseDetails(knowledgeBase.id);
|
||||
}
|
||||
fetchFiles();
|
||||
setShowEdit(false);
|
||||
};
|
||||
|
||||
const handleRecallTest = async () => {
|
||||
if (!recallQuery || !knowledgeBase?.id) return;
|
||||
setRecallLoading(true);
|
||||
try {
|
||||
const result = await retrieveKnowledgeBaseContent({
|
||||
query: recallQuery,
|
||||
topK: 10,
|
||||
threshold: 0.2,
|
||||
knowledgeBaseIds: [knowledgeBase.id],
|
||||
});
|
||||
setRecallResults(result?.data || []);
|
||||
} catch {
|
||||
setRecallResults([]);
|
||||
}
|
||||
setRecallLoading(false);
|
||||
};
|
||||
|
||||
const operations = [
|
||||
{
|
||||
key: "edit",
|
||||
label: "编辑知识库",
|
||||
icon: <EditOutlined className="w-4 h-4" />,
|
||||
onClick: () => {
|
||||
setShowEdit(true);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "refresh",
|
||||
label: "刷新知识库",
|
||||
icon: <ReloadOutlined className="w-4 h-4" />,
|
||||
onClick: () => {
|
||||
handleRefreshPage();
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "delete",
|
||||
label: "删除知识库",
|
||||
danger: true,
|
||||
confirm: {
|
||||
title: "确认删除该知识库吗?",
|
||||
description: "删除后将无法恢复,请谨慎操作。",
|
||||
cancelText: "取消",
|
||||
okText: "删除",
|
||||
okType: "danger",
|
||||
onConfirm: () => knowledgeBase && handleDeleteKB(knowledgeBase),
|
||||
},
|
||||
icon: <DeleteOutlined className="w-4 h-4" />,
|
||||
},
|
||||
];
|
||||
|
||||
const fileOps = [
|
||||
{
|
||||
key: "delete",
|
||||
label: "删除文件",
|
||||
icon: <DeleteOutlined className="w-4 h-4" />,
|
||||
danger: true,
|
||||
onClick: handleDeleteFile,
|
||||
},
|
||||
];
|
||||
|
||||
const fileColumns = [
|
||||
{
|
||||
title: "文件名",
|
||||
dataIndex: "name",
|
||||
key: "name",
|
||||
width: 200,
|
||||
ellipsis: true,
|
||||
fixed: "left" as const,
|
||||
},
|
||||
{
|
||||
title: "状态",
|
||||
dataIndex: "status",
|
||||
key: "vectorizationStatus",
|
||||
width: 120,
|
||||
render: (status: unknown) => {
|
||||
if (typeof status === 'object' && status !== null) {
|
||||
const s = status as { color?: string; label?: string };
|
||||
return <Badge color={s.color} text={s.label} />;
|
||||
}
|
||||
return <Badge color="default" text={String(status)} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "分块数",
|
||||
dataIndex: "chunkCount",
|
||||
key: "chunkCount",
|
||||
width: 100,
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: "创建时间",
|
||||
dataIndex: "createdAt",
|
||||
key: "createdAt",
|
||||
ellipsis: true,
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
title: "更新时间",
|
||||
dataIndex: "updatedAt",
|
||||
key: "updatedAt",
|
||||
ellipsis: true,
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
key: "actions",
|
||||
align: "right" as const,
|
||||
width: 100,
|
||||
render: (_: unknown, file: KBFile) => (
|
||||
<div>
|
||||
{fileOps.map((op) => (
|
||||
<Tooltip key={op.key} title={op.label}>
|
||||
<Button
|
||||
type="text"
|
||||
icon={op.icon}
|
||||
danger={op?.danger}
|
||||
onClick={() => op.onClick(file)}
|
||||
/>
|
||||
</Tooltip>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col">
|
||||
<div className="mb-4">
|
||||
<Breadcrumb>
|
||||
<Breadcrumb.Item>
|
||||
<a onClick={() => navigate("/data/knowledge-base")}>知识库</a>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Item>{knowledgeBase?.name}</Breadcrumb.Item>
|
||||
</Breadcrumb>
|
||||
</div>
|
||||
<DetailHeader
|
||||
data={knowledgeBase}
|
||||
statistics={knowledgeBase && Array.isArray((knowledgeBase as { statistics?: StatisticItem[] }).statistics)
|
||||
? ((knowledgeBase as { statistics?: StatisticItem[] }).statistics ?? [])
|
||||
: []}
|
||||
operations={operations}
|
||||
/>
|
||||
<CreateKnowledgeBase
|
||||
showBtn={false}
|
||||
isEdit={showEdit}
|
||||
data={knowledgeBase}
|
||||
onUpdate={handleRefreshPage}
|
||||
onClose={() => setShowEdit(false)}
|
||||
/>
|
||||
<div className="flex-1 border-card p-6 mt-4">
|
||||
<div className="flex items-center justify-between mb-4 gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Button type={activeTab === 'fileList' ? 'primary' : 'default'} onClick={() => setActiveTab('fileList')}>
|
||||
文件列表
|
||||
</Button>
|
||||
<Button type={activeTab === 'recallTest' ? 'primary' : 'default'} onClick={() => setActiveTab('recallTest')}>
|
||||
召回测试
|
||||
</Button>
|
||||
</div>
|
||||
{activeTab === 'fileList' && (
|
||||
<>
|
||||
<div className="flex-1">
|
||||
<SearchControls
|
||||
searchTerm={searchParams.keyword}
|
||||
onSearchChange={handleKeywordChange}
|
||||
searchPlaceholder="搜索文件名..."
|
||||
filters={[]}
|
||||
onFiltersChange={handleFiltersChange}
|
||||
onClearFilters={() => setSearchParams({ ...searchParams, filter: { type: [], status: [], tags: [] } })}
|
||||
showViewToggle={false}
|
||||
showReload={false}
|
||||
/>
|
||||
</div>
|
||||
<AddDataDialog knowledgeBase={knowledgeBase} onDataAdded={handleRefreshPage} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{activeTab === 'fileList' ? (
|
||||
<Table
|
||||
loading={loading}
|
||||
columns={fileColumns}
|
||||
dataSource={files}
|
||||
rowKey="id"
|
||||
pagination={pagination}
|
||||
scroll={{ y: "calc(100vh - 30rem)" }}
|
||||
/>
|
||||
) : (
|
||||
<div className="p-2">
|
||||
<div style={{ fontSize: 14, fontWeight: 300, marginBottom: 8 }}>基于语义文本检索和全文检索后的加权平均结果</div>
|
||||
<div className="flex items-center mb-4">
|
||||
<Input.Search
|
||||
value={recallQuery}
|
||||
onChange={e => setRecallQuery(e.target.value)}
|
||||
onSearch={handleRecallTest}
|
||||
placeholder="请输入召回测试问题"
|
||||
enterButton="检索"
|
||||
loading={recallLoading}
|
||||
style={{ width: "100%", fontSize: 18, height: 48 }}
|
||||
/>
|
||||
</div>
|
||||
{recallLoading ? (
|
||||
<Spin className="mt-8" />
|
||||
) : recallResults.length === 0 ? (
|
||||
<Empty description="暂无召回结果" />
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{recallResults.map((item, idx) => (
|
||||
<Card key={idx} title={`得分:${item.score?.toFixed(4) ?? "-"}`}
|
||||
extra={<span style={{ fontSize: 12 }}>ID: {item.entity?.id ?? "-"}</span>}
|
||||
style={{ wordBreak: "break-all" }}
|
||||
>
|
||||
<div style={{ marginBottom: 8, fontWeight: 500 }}>{item.entity?.text ?? ""}</div>
|
||||
<div style={{ fontSize: 12, color: '#888' }}>
|
||||
metadata: <pre style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-all', margin: 0 }}>{item.entity?.metadata}</pre>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default KnowledgeBaseDetailPage;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,180 +1,180 @@
|
||||
import { useState } from "react";
|
||||
import { Card, Button, Table, Tooltip, message } from "antd";
|
||||
import { DeleteOutlined, EditOutlined } from "@ant-design/icons";
|
||||
import { SearchControls } from "@/components/SearchControls";
|
||||
import { useNavigate } from "react-router";
|
||||
import CardView from "@/components/CardView";
|
||||
import {
|
||||
deleteKnowledgeBaseByIdUsingDelete,
|
||||
queryKnowledgeBasesUsingPost,
|
||||
} from "../knowledge-base.api";
|
||||
import useFetchData from "@/hooks/useFetchData";
|
||||
import { KnowledgeBaseItem } from "../knowledge-base.model";
|
||||
import CreateKnowledgeBase from "../components/CreateKnowledgeBase";
|
||||
import { mapKnowledgeBase } from "../knowledge-base.const";
|
||||
|
||||
export default function KnowledgeBasePage() {
|
||||
const navigate = useNavigate();
|
||||
const [viewMode, setViewMode] = useState<"card" | "list">("card");
|
||||
const [isEdit, setIsEdit] = useState(false);
|
||||
const [currentKB, setCurrentKB] = useState<KnowledgeBaseItem | null>(null);
|
||||
const {
|
||||
loading,
|
||||
tableData,
|
||||
searchParams,
|
||||
pagination,
|
||||
fetchData,
|
||||
setSearchParams,
|
||||
handleFiltersChange,
|
||||
handleKeywordChange,
|
||||
} = useFetchData<KnowledgeBaseItem>(
|
||||
queryKnowledgeBasesUsingPost,
|
||||
(kb) => mapKnowledgeBase(kb, false) // 在首页不显示索引模型和文本理解模型字段
|
||||
);
|
||||
|
||||
const handleDeleteKB = async (kb: KnowledgeBaseItem) => {
|
||||
try {
|
||||
await deleteKnowledgeBaseByIdUsingDelete(kb.id);
|
||||
message.success("知识库删除成功");
|
||||
fetchData();
|
||||
} catch (error) {
|
||||
message.error("知识库删除失败");
|
||||
}
|
||||
};
|
||||
|
||||
const operations = [
|
||||
{
|
||||
key: "edit",
|
||||
label: "编辑",
|
||||
icon: <EditOutlined />,
|
||||
onClick: (item) => {
|
||||
setIsEdit(true);
|
||||
setCurrentKB(item);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "delete",
|
||||
label: "删除",
|
||||
danger: true,
|
||||
icon: <DeleteOutlined />,
|
||||
confirm: {
|
||||
title: "确认删除",
|
||||
description: "此操作不可撤销,是否继续?",
|
||||
okText: "删除",
|
||||
okType: "danger",
|
||||
cancelText: "取消",
|
||||
},
|
||||
onClick: (item) => handleDeleteKB(item),
|
||||
},
|
||||
];
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: "知识库",
|
||||
dataIndex: "name",
|
||||
key: "name",
|
||||
fixed: "left" as const,
|
||||
width: 200,
|
||||
ellipsis: true,
|
||||
render: (_: any, kb: KnowledgeBaseItem) => (
|
||||
<Button
|
||||
type="link"
|
||||
onClick={() => navigate(`/data/knowledge-base/detail/${kb.id}`)}
|
||||
>
|
||||
{kb.name}
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "创建时间",
|
||||
dataIndex: "createdAt",
|
||||
key: "createdAt",
|
||||
ellipsis: true,
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
title: "更新时间",
|
||||
dataIndex: "updatedAt",
|
||||
key: "updatedAt",
|
||||
ellipsis: true,
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
title: "描述",
|
||||
dataIndex: "description",
|
||||
key: "description",
|
||||
width: 120,
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
key: "actions",
|
||||
fixed: "right" as const,
|
||||
width: 150,
|
||||
render: (_: any, kb: KnowledgeBaseItem) => (
|
||||
<div className="flex items-center gap-2">
|
||||
{operations.map((op) => (
|
||||
<Tooltip key={op.key} title={op.label}>
|
||||
<Button
|
||||
type="text"
|
||||
icon={op.icon}
|
||||
danger={op.danger}
|
||||
onClick={() => op.onClick(kb)}
|
||||
/>
|
||||
</Tooltip>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
// Main list view
|
||||
return (
|
||||
<div className="h-full flex flex-col gap-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="text-xl font-bold">知识生成</h1>
|
||||
<CreateKnowledgeBase
|
||||
isEdit={isEdit}
|
||||
data={currentKB}
|
||||
onUpdate={() => {
|
||||
fetchData();
|
||||
}}
|
||||
onClose={() => {
|
||||
setIsEdit(false);
|
||||
setCurrentKB(null);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<SearchControls
|
||||
searchTerm={searchParams.keyword}
|
||||
onSearchChange={handleKeywordChange}
|
||||
searchPlaceholder="搜索知识库..."
|
||||
filters={[]}
|
||||
onFiltersChange={handleFiltersChange}
|
||||
onClearFilters={() => setSearchParams({ ...searchParams, filter: {} })}
|
||||
viewMode={viewMode}
|
||||
onViewModeChange={setViewMode}
|
||||
showViewToggle
|
||||
onReload={fetchData}
|
||||
/>
|
||||
{viewMode === "card" ? (
|
||||
<CardView
|
||||
data={tableData}
|
||||
operations={operations}
|
||||
onView={(item) => navigate(`/data/knowledge-base/detail/${item.id}`)}
|
||||
pagination={pagination}
|
||||
/>
|
||||
) : (
|
||||
<Card>
|
||||
<Table
|
||||
loading={loading}
|
||||
scroll={{ x: "max-content", y: "calc(100vh - 20rem)" }}
|
||||
columns={columns}
|
||||
dataSource={tableData}
|
||||
rowKey="id"
|
||||
/>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
import { useState } from "react";
|
||||
import { Card, Button, Table, Tooltip, message } from "antd";
|
||||
import { DeleteOutlined, EditOutlined } from "@ant-design/icons";
|
||||
import { SearchControls } from "@/components/SearchControls";
|
||||
import { useNavigate } from "react-router";
|
||||
import CardView from "@/components/CardView";
|
||||
import {
|
||||
deleteKnowledgeBaseByIdUsingDelete,
|
||||
queryKnowledgeBasesUsingPost,
|
||||
} from "../knowledge-base.api";
|
||||
import useFetchData from "@/hooks/useFetchData";
|
||||
import { KnowledgeBaseItem } from "../knowledge-base.model";
|
||||
import CreateKnowledgeBase from "../components/CreateKnowledgeBase";
|
||||
import { mapKnowledgeBase } from "../knowledge-base.const";
|
||||
|
||||
export default function KnowledgeBasePage() {
|
||||
const navigate = useNavigate();
|
||||
const [viewMode, setViewMode] = useState<"card" | "list">("card");
|
||||
const [isEdit, setIsEdit] = useState(false);
|
||||
const [currentKB, setCurrentKB] = useState<KnowledgeBaseItem | null>(null);
|
||||
const {
|
||||
loading,
|
||||
tableData,
|
||||
searchParams,
|
||||
pagination,
|
||||
fetchData,
|
||||
setSearchParams,
|
||||
handleFiltersChange,
|
||||
handleKeywordChange,
|
||||
} = useFetchData<KnowledgeBaseItem>(
|
||||
queryKnowledgeBasesUsingPost,
|
||||
(kb) => mapKnowledgeBase(kb, false) // 在首页不显示索引模型和文本理解模型字段
|
||||
);
|
||||
|
||||
const handleDeleteKB = async (kb: KnowledgeBaseItem) => {
|
||||
try {
|
||||
await deleteKnowledgeBaseByIdUsingDelete(kb.id);
|
||||
message.success("知识库删除成功");
|
||||
fetchData();
|
||||
} catch (error) {
|
||||
message.error("知识库删除失败");
|
||||
}
|
||||
};
|
||||
|
||||
const operations = [
|
||||
{
|
||||
key: "edit",
|
||||
label: "编辑",
|
||||
icon: <EditOutlined />,
|
||||
onClick: (item) => {
|
||||
setIsEdit(true);
|
||||
setCurrentKB(item);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "delete",
|
||||
label: "删除",
|
||||
danger: true,
|
||||
icon: <DeleteOutlined />,
|
||||
confirm: {
|
||||
title: "确认删除",
|
||||
description: "此操作不可撤销,是否继续?",
|
||||
okText: "删除",
|
||||
okType: "danger",
|
||||
cancelText: "取消",
|
||||
},
|
||||
onClick: (item) => handleDeleteKB(item),
|
||||
},
|
||||
];
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: "知识库",
|
||||
dataIndex: "name",
|
||||
key: "name",
|
||||
fixed: "left" as const,
|
||||
width: 200,
|
||||
ellipsis: true,
|
||||
render: (_: any, kb: KnowledgeBaseItem) => (
|
||||
<Button
|
||||
type="link"
|
||||
onClick={() => navigate(`/data/knowledge-base/detail/${kb.id}`)}
|
||||
>
|
||||
{kb.name}
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "创建时间",
|
||||
dataIndex: "createdAt",
|
||||
key: "createdAt",
|
||||
ellipsis: true,
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
title: "更新时间",
|
||||
dataIndex: "updatedAt",
|
||||
key: "updatedAt",
|
||||
ellipsis: true,
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
title: "描述",
|
||||
dataIndex: "description",
|
||||
key: "description",
|
||||
width: 120,
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
key: "actions",
|
||||
fixed: "right" as const,
|
||||
width: 150,
|
||||
render: (_: any, kb: KnowledgeBaseItem) => (
|
||||
<div className="flex items-center gap-2">
|
||||
{operations.map((op) => (
|
||||
<Tooltip key={op.key} title={op.label}>
|
||||
<Button
|
||||
type="text"
|
||||
icon={op.icon}
|
||||
danger={op.danger}
|
||||
onClick={() => op.onClick(kb)}
|
||||
/>
|
||||
</Tooltip>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
// Main list view
|
||||
return (
|
||||
<div className="h-full flex flex-col gap-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="text-xl font-bold">知识生成</h1>
|
||||
<CreateKnowledgeBase
|
||||
isEdit={isEdit}
|
||||
data={currentKB}
|
||||
onUpdate={() => {
|
||||
fetchData();
|
||||
}}
|
||||
onClose={() => {
|
||||
setIsEdit(false);
|
||||
setCurrentKB(null);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<SearchControls
|
||||
searchTerm={searchParams.keyword}
|
||||
onSearchChange={handleKeywordChange}
|
||||
searchPlaceholder="搜索知识库..."
|
||||
filters={[]}
|
||||
onFiltersChange={handleFiltersChange}
|
||||
onClearFilters={() => setSearchParams({ ...searchParams, filter: {} })}
|
||||
viewMode={viewMode}
|
||||
onViewModeChange={setViewMode}
|
||||
showViewToggle
|
||||
onReload={fetchData}
|
||||
/>
|
||||
{viewMode === "card" ? (
|
||||
<CardView
|
||||
data={tableData}
|
||||
operations={operations}
|
||||
onView={(item) => navigate(`/data/knowledge-base/detail/${item.id}`)}
|
||||
pagination={pagination}
|
||||
/>
|
||||
) : (
|
||||
<Card>
|
||||
<Table
|
||||
loading={loading}
|
||||
scroll={{ x: "max-content", y: "calc(100vh - 20rem)" }}
|
||||
columns={columns}
|
||||
dataSource={tableData}
|
||||
rowKey="id"
|
||||
/>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,353 +1,353 @@
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Button,
|
||||
App,
|
||||
Input,
|
||||
Select,
|
||||
Form,
|
||||
Modal,
|
||||
Steps,
|
||||
Descriptions,
|
||||
Table,
|
||||
} from "antd";
|
||||
import { PlusOutlined } from "@ant-design/icons";
|
||||
import { addKnowledgeBaseFilesUsingPost } from "../knowledge-base.api";
|
||||
import DatasetFileTransfer from "@/components/business/DatasetFileTransfer";
|
||||
import { DescriptionsItemType } from "antd/es/descriptions";
|
||||
import { DatasetFileCols } from "../knowledge-base.const";
|
||||
|
||||
const sliceOptions = [
|
||||
{ label: "默认分块", value: "DEFAULT_CHUNK" },
|
||||
{ label: "章节分块", value: "CHAPTER_CHUNK" },
|
||||
{ label: "段落分块", value: "PARAGRAPH_CHUNK" },
|
||||
{ label: "长度分块", value: "LENGTH_CHUNK" },
|
||||
{ label: "自定义分割符分块", value: "CUSTOM_SEPARATOR_CHUNK" },
|
||||
];
|
||||
|
||||
export default function AddDataDialog({ knowledgeBase, onDataAdded }) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const { message } = App.useApp();
|
||||
const [form] = Form.useForm();
|
||||
const [currentStep, setCurrentStep] = useState(0);
|
||||
|
||||
const [selectedFilesMap, setSelectedFilesMap] = useState({});
|
||||
|
||||
// 定义分块选项
|
||||
const sliceOptions = [
|
||||
{ label: "默认分块", value: "DEFAULT_CHUNK" },
|
||||
{ label: "按章节分块", value: "CHAPTER_CHUNK" },
|
||||
{ label: "按段落分块", value: "PARAGRAPH_CHUNK" },
|
||||
{ label: "固定长度分块", value: "FIXED_LENGTH_CHUNK" },
|
||||
{ label: "自定义分隔符分块", value: "CUSTOM_SEPARATOR_CHUNK" },
|
||||
];
|
||||
|
||||
// 定义初始状态
|
||||
const [newKB, setNewKB] = useState({
|
||||
processType: "DEFAULT_CHUNK",
|
||||
chunkSize: 500,
|
||||
overlapSize: 50,
|
||||
delimiter: "",
|
||||
});
|
||||
|
||||
const steps = [
|
||||
{
|
||||
title: "选择数据集文件",
|
||||
description: "从多个数据集中选择文件",
|
||||
},
|
||||
{
|
||||
title: "配置参数",
|
||||
description: "设置数据处理参数",
|
||||
},
|
||||
{
|
||||
title: "确认上传",
|
||||
description: "确认信息并上传",
|
||||
},
|
||||
];
|
||||
|
||||
// 获取已选择文件总数
|
||||
const getSelectedFilesCount = () => {
|
||||
return Object.values(selectedFilesMap).reduce(
|
||||
(total, ids) => total + ids.length,
|
||||
0
|
||||
);
|
||||
};
|
||||
|
||||
const handleNext = () => {
|
||||
// 验证当前步骤
|
||||
if (currentStep === 0) {
|
||||
if (getSelectedFilesCount() === 0) {
|
||||
message.warning("请至少选择一个文件");
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (currentStep === 1) {
|
||||
// 验证切片参数
|
||||
if (!newKB.processType) {
|
||||
message.warning("请选择分块方式");
|
||||
return;
|
||||
}
|
||||
if (!newKB.chunkSize || Number(newKB.chunkSize) <= 0) {
|
||||
message.warning("请输入有效的分块大小");
|
||||
return;
|
||||
}
|
||||
if (!newKB.overlapSize || Number(newKB.overlapSize) < 0) {
|
||||
message.warning("请输入有效的重叠长度");
|
||||
return;
|
||||
}
|
||||
if (newKB.processType === "CUSTOM_SEPARATOR_CHUNK" && !newKB.delimiter) {
|
||||
message.warning("请输入分隔符");
|
||||
return;
|
||||
}
|
||||
}
|
||||
setCurrentStep(currentStep + 1);
|
||||
};
|
||||
|
||||
const handlePrev = () => {
|
||||
setCurrentStep(currentStep - 1);
|
||||
};
|
||||
|
||||
// 重置所有状态
|
||||
const handleReset = () => {
|
||||
setCurrentStep(0);
|
||||
setNewKB({
|
||||
processType: "DEFAULT_CHUNK",
|
||||
chunkSize: 500,
|
||||
overlapSize: 50,
|
||||
delimiter: "",
|
||||
});
|
||||
form.resetFields();
|
||||
setSelectedFilesMap({});
|
||||
};
|
||||
|
||||
const handleAddData = async () => {
|
||||
if (getSelectedFilesCount() === 0) {
|
||||
message.warning("请至少选择一个文件");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 构造符合API要求的请求数据
|
||||
const requestData = {
|
||||
files: Object.values(selectedFilesMap),
|
||||
processType: newKB.processType,
|
||||
chunkSize: Number(newKB.chunkSize), // 确保是数字类型
|
||||
overlapSize: Number(newKB.overlapSize), // 确保是数字类型
|
||||
delimiter: newKB.delimiter,
|
||||
};
|
||||
|
||||
await addKnowledgeBaseFilesUsingPost(knowledgeBase.id, requestData);
|
||||
|
||||
// 先通知父组件刷新数据(确保刷新发生在重置前)
|
||||
onDataAdded?.();
|
||||
|
||||
message.success("数据添加成功");
|
||||
// 重置状态
|
||||
setOpen(false);
|
||||
} catch (error) {
|
||||
message.error("数据添加失败,请重试");
|
||||
console.error("添加文件失败:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleModalCancel = () => {
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
const descItems: DescriptionsItemType[] = [
|
||||
{
|
||||
label: "知识库名称",
|
||||
key: "knowledgeBaseName",
|
||||
children: knowledgeBase?.name,
|
||||
},
|
||||
{
|
||||
label: "数据来源",
|
||||
key: "dataSource",
|
||||
children: "数据集",
|
||||
},
|
||||
{
|
||||
label: "文件总数",
|
||||
key: "totalFileCount",
|
||||
children: Object.keys(selectedFilesMap).length,
|
||||
},
|
||||
{
|
||||
label: "分块方式",
|
||||
key: "chunkingMethod",
|
||||
children:
|
||||
sliceOptions.find((opt) => opt.value === newKB.processType)?.label ||
|
||||
"",
|
||||
},
|
||||
{
|
||||
label: "分块大小",
|
||||
key: "chunkSize",
|
||||
children: newKB.chunkSize,
|
||||
},
|
||||
{
|
||||
label: "重叠长度",
|
||||
key: "overlapSize",
|
||||
children: newKB.overlapSize,
|
||||
},
|
||||
...(newKB.processType === "CUSTOM_SEPARATOR_CHUNK" && newKB.delimiter
|
||||
? [
|
||||
{
|
||||
label: "分隔符",
|
||||
children: <span className="font-mono">{newKB.delimiter}</span>,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
label: "文件列表",
|
||||
key: "fileList",
|
||||
span: 3,
|
||||
children: (
|
||||
<Table
|
||||
scroll={{ y: 400 }}
|
||||
rowKey="id"
|
||||
size="small"
|
||||
dataSource={Object.values(selectedFilesMap)}
|
||||
columns={DatasetFileCols}
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => {
|
||||
handleReset();
|
||||
setOpen(true);
|
||||
}}
|
||||
>
|
||||
添加数据
|
||||
</Button>
|
||||
<Modal
|
||||
title="添加数据"
|
||||
open={open}
|
||||
onCancel={handleModalCancel}
|
||||
footer={
|
||||
<div className="space-x-2">
|
||||
{currentStep === 0 && (
|
||||
<Button onClick={handleModalCancel}>取消</Button>
|
||||
)}
|
||||
{currentStep > 0 && (
|
||||
<Button disabled={false} onClick={handlePrev}>
|
||||
上一步
|
||||
</Button>
|
||||
)}
|
||||
{currentStep < steps.length - 1 ? (
|
||||
<Button
|
||||
type="primary"
|
||||
disabled={
|
||||
Object.keys(selectedFilesMap).length === 0 ||
|
||||
!newKB.chunkSize ||
|
||||
!newKB.overlapSize ||
|
||||
!newKB.processType
|
||||
}
|
||||
onClick={handleNext}
|
||||
>
|
||||
下一步
|
||||
</Button>
|
||||
) : (
|
||||
<Button type="primary" onClick={handleAddData}>
|
||||
确认上传
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
width={1000}
|
||||
>
|
||||
<div>
|
||||
{/* 步骤导航 */}
|
||||
<Steps
|
||||
current={currentStep}
|
||||
size="small"
|
||||
items={steps}
|
||||
labelPlacement="vertical"
|
||||
/>
|
||||
|
||||
{/* 步骤内容 */}
|
||||
{currentStep === 0 && (
|
||||
<DatasetFileTransfer
|
||||
open={open}
|
||||
selectedFilesMap={selectedFilesMap}
|
||||
onSelectedFilesChange={setSelectedFilesMap}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Form
|
||||
hidden={currentStep !== 1}
|
||||
form={form}
|
||||
layout="vertical"
|
||||
initialValues={newKB}
|
||||
onValuesChange={(_, allValues) => setNewKB(allValues)}
|
||||
>
|
||||
<div className="space-y-6">
|
||||
<Form.Item
|
||||
label="分块方式"
|
||||
name="processType"
|
||||
required
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Select options={sliceOptions} />
|
||||
</Form.Item>
|
||||
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
<Form.Item
|
||||
label="分块大小"
|
||||
name="chunkSize"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: "请输入分块大小",
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input type="number" placeholder="请输入分块大小" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="重叠长度"
|
||||
name="overlapSize"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: "请输入重叠长度",
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input type="number" placeholder="请输入重叠长度" />
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
{newKB.processType === "CUSTOM_SEPARATOR_CHUNK" && (
|
||||
<Form.Item
|
||||
label="分隔符"
|
||||
name="delimiter"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: "请输入分隔符",
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input placeholder="输入分隔符,如 \n\n" />
|
||||
</Form.Item>
|
||||
)}
|
||||
</div>
|
||||
</Form>
|
||||
|
||||
<div className="space-y-6" hidden={currentStep !== 2}>
|
||||
<div className="">
|
||||
<div className="text-lg font-medium mb-3">上传信息确认</div>
|
||||
<Descriptions layout="vertical" size="small" items={descItems} />
|
||||
</div>
|
||||
<div className="text-sm text-yellow-600">
|
||||
提示:上传后系统将自动处理文件,请耐心等待
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Button,
|
||||
App,
|
||||
Input,
|
||||
Select,
|
||||
Form,
|
||||
Modal,
|
||||
Steps,
|
||||
Descriptions,
|
||||
Table,
|
||||
} from "antd";
|
||||
import { PlusOutlined } from "@ant-design/icons";
|
||||
import { addKnowledgeBaseFilesUsingPost } from "../knowledge-base.api";
|
||||
import DatasetFileTransfer from "@/components/business/DatasetFileTransfer";
|
||||
import { DescriptionsItemType } from "antd/es/descriptions";
|
||||
import { DatasetFileCols } from "../knowledge-base.const";
|
||||
|
||||
const sliceOptions = [
|
||||
{ label: "默认分块", value: "DEFAULT_CHUNK" },
|
||||
{ label: "章节分块", value: "CHAPTER_CHUNK" },
|
||||
{ label: "段落分块", value: "PARAGRAPH_CHUNK" },
|
||||
{ label: "长度分块", value: "LENGTH_CHUNK" },
|
||||
{ label: "自定义分割符分块", value: "CUSTOM_SEPARATOR_CHUNK" },
|
||||
];
|
||||
|
||||
export default function AddDataDialog({ knowledgeBase, onDataAdded }) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const { message } = App.useApp();
|
||||
const [form] = Form.useForm();
|
||||
const [currentStep, setCurrentStep] = useState(0);
|
||||
|
||||
const [selectedFilesMap, setSelectedFilesMap] = useState({});
|
||||
|
||||
// 定义分块选项
|
||||
const sliceOptions = [
|
||||
{ label: "默认分块", value: "DEFAULT_CHUNK" },
|
||||
{ label: "按章节分块", value: "CHAPTER_CHUNK" },
|
||||
{ label: "按段落分块", value: "PARAGRAPH_CHUNK" },
|
||||
{ label: "固定长度分块", value: "FIXED_LENGTH_CHUNK" },
|
||||
{ label: "自定义分隔符分块", value: "CUSTOM_SEPARATOR_CHUNK" },
|
||||
];
|
||||
|
||||
// 定义初始状态
|
||||
const [newKB, setNewKB] = useState({
|
||||
processType: "DEFAULT_CHUNK",
|
||||
chunkSize: 500,
|
||||
overlapSize: 50,
|
||||
delimiter: "",
|
||||
});
|
||||
|
||||
const steps = [
|
||||
{
|
||||
title: "选择数据集文件",
|
||||
description: "从多个数据集中选择文件",
|
||||
},
|
||||
{
|
||||
title: "配置参数",
|
||||
description: "设置数据处理参数",
|
||||
},
|
||||
{
|
||||
title: "确认上传",
|
||||
description: "确认信息并上传",
|
||||
},
|
||||
];
|
||||
|
||||
// 获取已选择文件总数
|
||||
const getSelectedFilesCount = () => {
|
||||
return Object.values(selectedFilesMap).reduce(
|
||||
(total, ids) => total + ids.length,
|
||||
0
|
||||
);
|
||||
};
|
||||
|
||||
const handleNext = () => {
|
||||
// 验证当前步骤
|
||||
if (currentStep === 0) {
|
||||
if (getSelectedFilesCount() === 0) {
|
||||
message.warning("请至少选择一个文件");
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (currentStep === 1) {
|
||||
// 验证切片参数
|
||||
if (!newKB.processType) {
|
||||
message.warning("请选择分块方式");
|
||||
return;
|
||||
}
|
||||
if (!newKB.chunkSize || Number(newKB.chunkSize) <= 0) {
|
||||
message.warning("请输入有效的分块大小");
|
||||
return;
|
||||
}
|
||||
if (!newKB.overlapSize || Number(newKB.overlapSize) < 0) {
|
||||
message.warning("请输入有效的重叠长度");
|
||||
return;
|
||||
}
|
||||
if (newKB.processType === "CUSTOM_SEPARATOR_CHUNK" && !newKB.delimiter) {
|
||||
message.warning("请输入分隔符");
|
||||
return;
|
||||
}
|
||||
}
|
||||
setCurrentStep(currentStep + 1);
|
||||
};
|
||||
|
||||
const handlePrev = () => {
|
||||
setCurrentStep(currentStep - 1);
|
||||
};
|
||||
|
||||
// 重置所有状态
|
||||
const handleReset = () => {
|
||||
setCurrentStep(0);
|
||||
setNewKB({
|
||||
processType: "DEFAULT_CHUNK",
|
||||
chunkSize: 500,
|
||||
overlapSize: 50,
|
||||
delimiter: "",
|
||||
});
|
||||
form.resetFields();
|
||||
setSelectedFilesMap({});
|
||||
};
|
||||
|
||||
const handleAddData = async () => {
|
||||
if (getSelectedFilesCount() === 0) {
|
||||
message.warning("请至少选择一个文件");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 构造符合API要求的请求数据
|
||||
const requestData = {
|
||||
files: Object.values(selectedFilesMap),
|
||||
processType: newKB.processType,
|
||||
chunkSize: Number(newKB.chunkSize), // 确保是数字类型
|
||||
overlapSize: Number(newKB.overlapSize), // 确保是数字类型
|
||||
delimiter: newKB.delimiter,
|
||||
};
|
||||
|
||||
await addKnowledgeBaseFilesUsingPost(knowledgeBase.id, requestData);
|
||||
|
||||
// 先通知父组件刷新数据(确保刷新发生在重置前)
|
||||
onDataAdded?.();
|
||||
|
||||
message.success("数据添加成功");
|
||||
// 重置状态
|
||||
setOpen(false);
|
||||
} catch (error) {
|
||||
message.error("数据添加失败,请重试");
|
||||
console.error("添加文件失败:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleModalCancel = () => {
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
const descItems: DescriptionsItemType[] = [
|
||||
{
|
||||
label: "知识库名称",
|
||||
key: "knowledgeBaseName",
|
||||
children: knowledgeBase?.name,
|
||||
},
|
||||
{
|
||||
label: "数据来源",
|
||||
key: "dataSource",
|
||||
children: "数据集",
|
||||
},
|
||||
{
|
||||
label: "文件总数",
|
||||
key: "totalFileCount",
|
||||
children: Object.keys(selectedFilesMap).length,
|
||||
},
|
||||
{
|
||||
label: "分块方式",
|
||||
key: "chunkingMethod",
|
||||
children:
|
||||
sliceOptions.find((opt) => opt.value === newKB.processType)?.label ||
|
||||
"",
|
||||
},
|
||||
{
|
||||
label: "分块大小",
|
||||
key: "chunkSize",
|
||||
children: newKB.chunkSize,
|
||||
},
|
||||
{
|
||||
label: "重叠长度",
|
||||
key: "overlapSize",
|
||||
children: newKB.overlapSize,
|
||||
},
|
||||
...(newKB.processType === "CUSTOM_SEPARATOR_CHUNK" && newKB.delimiter
|
||||
? [
|
||||
{
|
||||
label: "分隔符",
|
||||
children: <span className="font-mono">{newKB.delimiter}</span>,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
label: "文件列表",
|
||||
key: "fileList",
|
||||
span: 3,
|
||||
children: (
|
||||
<Table
|
||||
scroll={{ y: 400 }}
|
||||
rowKey="id"
|
||||
size="small"
|
||||
dataSource={Object.values(selectedFilesMap)}
|
||||
columns={DatasetFileCols}
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => {
|
||||
handleReset();
|
||||
setOpen(true);
|
||||
}}
|
||||
>
|
||||
添加数据
|
||||
</Button>
|
||||
<Modal
|
||||
title="添加数据"
|
||||
open={open}
|
||||
onCancel={handleModalCancel}
|
||||
footer={
|
||||
<div className="space-x-2">
|
||||
{currentStep === 0 && (
|
||||
<Button onClick={handleModalCancel}>取消</Button>
|
||||
)}
|
||||
{currentStep > 0 && (
|
||||
<Button disabled={false} onClick={handlePrev}>
|
||||
上一步
|
||||
</Button>
|
||||
)}
|
||||
{currentStep < steps.length - 1 ? (
|
||||
<Button
|
||||
type="primary"
|
||||
disabled={
|
||||
Object.keys(selectedFilesMap).length === 0 ||
|
||||
!newKB.chunkSize ||
|
||||
!newKB.overlapSize ||
|
||||
!newKB.processType
|
||||
}
|
||||
onClick={handleNext}
|
||||
>
|
||||
下一步
|
||||
</Button>
|
||||
) : (
|
||||
<Button type="primary" onClick={handleAddData}>
|
||||
确认上传
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
width={1000}
|
||||
>
|
||||
<div>
|
||||
{/* 步骤导航 */}
|
||||
<Steps
|
||||
current={currentStep}
|
||||
size="small"
|
||||
items={steps}
|
||||
labelPlacement="vertical"
|
||||
/>
|
||||
|
||||
{/* 步骤内容 */}
|
||||
{currentStep === 0 && (
|
||||
<DatasetFileTransfer
|
||||
open={open}
|
||||
selectedFilesMap={selectedFilesMap}
|
||||
onSelectedFilesChange={setSelectedFilesMap}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Form
|
||||
hidden={currentStep !== 1}
|
||||
form={form}
|
||||
layout="vertical"
|
||||
initialValues={newKB}
|
||||
onValuesChange={(_, allValues) => setNewKB(allValues)}
|
||||
>
|
||||
<div className="space-y-6">
|
||||
<Form.Item
|
||||
label="分块方式"
|
||||
name="processType"
|
||||
required
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Select options={sliceOptions} />
|
||||
</Form.Item>
|
||||
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
<Form.Item
|
||||
label="分块大小"
|
||||
name="chunkSize"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: "请输入分块大小",
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input type="number" placeholder="请输入分块大小" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="重叠长度"
|
||||
name="overlapSize"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: "请输入重叠长度",
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input type="number" placeholder="请输入重叠长度" />
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
{newKB.processType === "CUSTOM_SEPARATOR_CHUNK" && (
|
||||
<Form.Item
|
||||
label="分隔符"
|
||||
name="delimiter"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: "请输入分隔符",
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input placeholder="输入分隔符,如 \n\n" />
|
||||
</Form.Item>
|
||||
)}
|
||||
</div>
|
||||
</Form>
|
||||
|
||||
<div className="space-y-6" hidden={currentStep !== 2}>
|
||||
<div className="">
|
||||
<div className="text-lg font-medium mb-3">上传信息确认</div>
|
||||
<Descriptions layout="vertical" size="small" items={descItems} />
|
||||
</div>
|
||||
<div className="text-sm text-yellow-600">
|
||||
提示:上传后系统将自动处理文件,请耐心等待
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,178 +1,178 @@
|
||||
import { Button, Form, Input, message, Modal, Select } from "antd";
|
||||
import { PlusOutlined } from "@ant-design/icons";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { queryModelListUsingGet } from "@/pages/SettingsPage/settings.apis";
|
||||
import { ModelI } from "@/pages/SettingsPage/ModelAccess";
|
||||
import {
|
||||
createKnowledgeBaseUsingPost,
|
||||
updateKnowledgeBaseByIdUsingPut,
|
||||
} from "../knowledge-base.api";
|
||||
import { KnowledgeBaseItem } from "../knowledge-base.model";
|
||||
import { showSettings } from "@/store/slices/settingsSlice";
|
||||
|
||||
export default function CreateKnowledgeBase({
|
||||
isEdit,
|
||||
data,
|
||||
showBtn = true,
|
||||
onUpdate,
|
||||
onClose,
|
||||
}: {
|
||||
isEdit?: boolean;
|
||||
showBtn?: boolean;
|
||||
data?: Partial<KnowledgeBaseItem> | null;
|
||||
onUpdate: () => void;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [form] = Form.useForm();
|
||||
const [models, setModels] = useState<ModelI[]>([]);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const embeddingModelOptions = models
|
||||
.filter((model) => model.type === "EMBEDDING")
|
||||
.map((model) => ({
|
||||
label: model.modelName + " (" + model.provider + ")",
|
||||
value: model.id,
|
||||
}));
|
||||
|
||||
const chatModelOptions = models
|
||||
.filter((model) => model.type === "CHAT")
|
||||
.map((model) => ({
|
||||
label: model.modelName + " (" + model.provider + ")",
|
||||
value: model.id,
|
||||
}));
|
||||
|
||||
const fetchModels = async () => {
|
||||
const { data } = await queryModelListUsingGet({ page: 0, size: 1000 });
|
||||
setModels(data.content || []);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (open) fetchModels();
|
||||
}, [open]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isEdit && data) {
|
||||
setOpen(true);
|
||||
form.setFieldsValue({
|
||||
name: data.name,
|
||||
description: data.description,
|
||||
embeddingModel: data.embeddingModel,
|
||||
chatModel: data.chatModel,
|
||||
});
|
||||
}
|
||||
}, [isEdit, data, form]);
|
||||
|
||||
const handleCreateKnowledgeBase = async () => {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
if (isEdit && data) {
|
||||
await updateKnowledgeBaseByIdUsingPut(data.id!, values);
|
||||
message.success("知识库更新成功");
|
||||
} else {
|
||||
await createKnowledgeBaseUsingPost(values);
|
||||
message.success("知识库创建成功");
|
||||
}
|
||||
setOpen(false);
|
||||
onUpdate();
|
||||
} catch (error) {
|
||||
message.error("操作失败:", error.data.message);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCloseModal = () => {
|
||||
setOpen(false);
|
||||
onClose?.();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{showBtn && (
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => {
|
||||
form.resetFields();
|
||||
setOpen(true);
|
||||
}}
|
||||
>
|
||||
创建知识库
|
||||
</Button>
|
||||
)}
|
||||
<Modal
|
||||
title={isEdit ? "编辑知识库" : "创建知识库"}
|
||||
open={open}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
maskClosable={false}
|
||||
onCancel={handleCloseModal}
|
||||
onOk={handleCreateKnowledgeBase}
|
||||
>
|
||||
<Form form={form} layout="vertical">
|
||||
<Form.Item
|
||||
label="知识库名称"
|
||||
name="name"
|
||||
rules={[{ required: true, message: "请输入知识库名称" }]}
|
||||
>
|
||||
<Input placeholder="请输入知识库名称" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="描述"
|
||||
name="description"
|
||||
rules={[{ required: false }]}
|
||||
>
|
||||
<Input.TextArea placeholder="请输入知识库描述(可选)" rows={4} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="索引模型"
|
||||
name="embeddingModel"
|
||||
rules={[{ required: true, message: "请选择索引模型" }]}
|
||||
>
|
||||
<Select
|
||||
placeholder="请选择索引模型"
|
||||
options={embeddingModelOptions}
|
||||
disabled={isEdit} // 编辑模式下禁用索引模型修改
|
||||
popupRender={(menu) => (
|
||||
<>
|
||||
{menu}
|
||||
<Button
|
||||
block
|
||||
type="link"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => dispatch(showSettings())}
|
||||
>
|
||||
添加模型
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="文本理解模型"
|
||||
name="chatModel"
|
||||
rules={[{ required: true, message: "请选择文本理解模型" }]}
|
||||
>
|
||||
<Select
|
||||
placeholder="请选择文本理解模型"
|
||||
options={chatModelOptions}
|
||||
popupRender={(menu) => (
|
||||
<>
|
||||
{menu}
|
||||
<Button
|
||||
block
|
||||
type="link"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => dispatch(showSettings())}
|
||||
>
|
||||
添加模型
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
import { Button, Form, Input, message, Modal, Select } from "antd";
|
||||
import { PlusOutlined } from "@ant-design/icons";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { queryModelListUsingGet } from "@/pages/SettingsPage/settings.apis";
|
||||
import { ModelI } from "@/pages/SettingsPage/ModelAccess";
|
||||
import {
|
||||
createKnowledgeBaseUsingPost,
|
||||
updateKnowledgeBaseByIdUsingPut,
|
||||
} from "../knowledge-base.api";
|
||||
import { KnowledgeBaseItem } from "../knowledge-base.model";
|
||||
import { showSettings } from "@/store/slices/settingsSlice";
|
||||
|
||||
export default function CreateKnowledgeBase({
|
||||
isEdit,
|
||||
data,
|
||||
showBtn = true,
|
||||
onUpdate,
|
||||
onClose,
|
||||
}: {
|
||||
isEdit?: boolean;
|
||||
showBtn?: boolean;
|
||||
data?: Partial<KnowledgeBaseItem> | null;
|
||||
onUpdate: () => void;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [form] = Form.useForm();
|
||||
const [models, setModels] = useState<ModelI[]>([]);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const embeddingModelOptions = models
|
||||
.filter((model) => model.type === "EMBEDDING")
|
||||
.map((model) => ({
|
||||
label: model.modelName + " (" + model.provider + ")",
|
||||
value: model.id,
|
||||
}));
|
||||
|
||||
const chatModelOptions = models
|
||||
.filter((model) => model.type === "CHAT")
|
||||
.map((model) => ({
|
||||
label: model.modelName + " (" + model.provider + ")",
|
||||
value: model.id,
|
||||
}));
|
||||
|
||||
const fetchModels = async () => {
|
||||
const { data } = await queryModelListUsingGet({ page: 0, size: 1000 });
|
||||
setModels(data.content || []);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (open) fetchModels();
|
||||
}, [open]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isEdit && data) {
|
||||
setOpen(true);
|
||||
form.setFieldsValue({
|
||||
name: data.name,
|
||||
description: data.description,
|
||||
embeddingModel: data.embeddingModel,
|
||||
chatModel: data.chatModel,
|
||||
});
|
||||
}
|
||||
}, [isEdit, data, form]);
|
||||
|
||||
const handleCreateKnowledgeBase = async () => {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
if (isEdit && data) {
|
||||
await updateKnowledgeBaseByIdUsingPut(data.id!, values);
|
||||
message.success("知识库更新成功");
|
||||
} else {
|
||||
await createKnowledgeBaseUsingPost(values);
|
||||
message.success("知识库创建成功");
|
||||
}
|
||||
setOpen(false);
|
||||
onUpdate();
|
||||
} catch (error) {
|
||||
message.error("操作失败:", error.data.message);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCloseModal = () => {
|
||||
setOpen(false);
|
||||
onClose?.();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{showBtn && (
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => {
|
||||
form.resetFields();
|
||||
setOpen(true);
|
||||
}}
|
||||
>
|
||||
创建知识库
|
||||
</Button>
|
||||
)}
|
||||
<Modal
|
||||
title={isEdit ? "编辑知识库" : "创建知识库"}
|
||||
open={open}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
maskClosable={false}
|
||||
onCancel={handleCloseModal}
|
||||
onOk={handleCreateKnowledgeBase}
|
||||
>
|
||||
<Form form={form} layout="vertical">
|
||||
<Form.Item
|
||||
label="知识库名称"
|
||||
name="name"
|
||||
rules={[{ required: true, message: "请输入知识库名称" }]}
|
||||
>
|
||||
<Input placeholder="请输入知识库名称" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="描述"
|
||||
name="description"
|
||||
rules={[{ required: false }]}
|
||||
>
|
||||
<Input.TextArea placeholder="请输入知识库描述(可选)" rows={4} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="索引模型"
|
||||
name="embeddingModel"
|
||||
rules={[{ required: true, message: "请选择索引模型" }]}
|
||||
>
|
||||
<Select
|
||||
placeholder="请选择索引模型"
|
||||
options={embeddingModelOptions}
|
||||
disabled={isEdit} // 编辑模式下禁用索引模型修改
|
||||
popupRender={(menu) => (
|
||||
<>
|
||||
{menu}
|
||||
<Button
|
||||
block
|
||||
type="link"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => dispatch(showSettings())}
|
||||
>
|
||||
添加模型
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="文本理解模型"
|
||||
name="chatModel"
|
||||
rules={[{ required: true, message: "请选择文本理解模型" }]}
|
||||
>
|
||||
<Select
|
||||
placeholder="请选择文本理解模型"
|
||||
options={chatModelOptions}
|
||||
popupRender={(menu) => (
|
||||
<>
|
||||
{menu}
|
||||
<Button
|
||||
block
|
||||
type="link"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => dispatch(showSettings())}
|
||||
>
|
||||
添加模型
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,51 +1,51 @@
|
||||
import { get, post, put, del } from "@/utils/request";
|
||||
|
||||
// 获取知识库列表
|
||||
export function queryKnowledgeBasesUsingPost(params: any) {
|
||||
return post("/api/knowledge-base/list", params);
|
||||
}
|
||||
|
||||
// 创建知识库
|
||||
export function createKnowledgeBaseUsingPost(data: any) {
|
||||
return post("/api/knowledge-base/create", data);
|
||||
}
|
||||
|
||||
// 获取知识库详情
|
||||
export function queryKnowledgeBaseByIdUsingGet(baseId: string) {
|
||||
return get(`/api/knowledge-base/${baseId}`);
|
||||
}
|
||||
|
||||
// 更新知识库
|
||||
export function updateKnowledgeBaseByIdUsingPut(baseId: string, data: any) {
|
||||
return put(`/api/knowledge-base/${baseId}`, data);
|
||||
}
|
||||
|
||||
// 删除知识库
|
||||
export function deleteKnowledgeBaseByIdUsingDelete(baseId: string) {
|
||||
return del(`/api/knowledge-base/${baseId}`);
|
||||
}
|
||||
|
||||
// 获取知识生成文件列表
|
||||
export function queryKnowledgeBaseFilesUsingGet(baseId: string, data) {
|
||||
return get(`/api/knowledge-base/${baseId}/files`, data);
|
||||
}
|
||||
|
||||
// 添加文件到知识库
|
||||
export function addKnowledgeBaseFilesUsingPost(baseId: string, data: any) {
|
||||
return post(`/api/knowledge-base/${baseId}/files`, data);
|
||||
}
|
||||
|
||||
// 删除知识生成文件
|
||||
export function deleteKnowledgeBaseFileByIdUsingDelete(baseId: string, data: any) {
|
||||
return del(`/api/knowledge-base/${baseId}/files`, data);
|
||||
}
|
||||
|
||||
// 检索知识库内容
|
||||
export function retrieveKnowledgeBaseContent(data: {
|
||||
query: string;
|
||||
topK?: number;
|
||||
threshold?: number;
|
||||
knowledgeBaseIds: string[];
|
||||
}) {
|
||||
return post("/api/knowledge-base/retrieve", data);
|
||||
}
|
||||
import { get, post, put, del } from "@/utils/request";
|
||||
|
||||
// 获取知识库列表
|
||||
export function queryKnowledgeBasesUsingPost(params: any) {
|
||||
return post("/api/knowledge-base/list", params);
|
||||
}
|
||||
|
||||
// 创建知识库
|
||||
export function createKnowledgeBaseUsingPost(data: any) {
|
||||
return post("/api/knowledge-base/create", data);
|
||||
}
|
||||
|
||||
// 获取知识库详情
|
||||
export function queryKnowledgeBaseByIdUsingGet(baseId: string) {
|
||||
return get(`/api/knowledge-base/${baseId}`);
|
||||
}
|
||||
|
||||
// 更新知识库
|
||||
export function updateKnowledgeBaseByIdUsingPut(baseId: string, data: any) {
|
||||
return put(`/api/knowledge-base/${baseId}`, data);
|
||||
}
|
||||
|
||||
// 删除知识库
|
||||
export function deleteKnowledgeBaseByIdUsingDelete(baseId: string) {
|
||||
return del(`/api/knowledge-base/${baseId}`);
|
||||
}
|
||||
|
||||
// 获取知识生成文件列表
|
||||
export function queryKnowledgeBaseFilesUsingGet(baseId: string, data) {
|
||||
return get(`/api/knowledge-base/${baseId}/files`, data);
|
||||
}
|
||||
|
||||
// 添加文件到知识库
|
||||
export function addKnowledgeBaseFilesUsingPost(baseId: string, data: any) {
|
||||
return post(`/api/knowledge-base/${baseId}/files`, data);
|
||||
}
|
||||
|
||||
// 删除知识生成文件
|
||||
export function deleteKnowledgeBaseFileByIdUsingDelete(baseId: string, data: any) {
|
||||
return del(`/api/knowledge-base/${baseId}/files`, data);
|
||||
}
|
||||
|
||||
// 检索知识库内容
|
||||
export function retrieveKnowledgeBaseContent(data: {
|
||||
query: string;
|
||||
topK?: number;
|
||||
threshold?: number;
|
||||
knowledgeBaseIds: string[];
|
||||
}) {
|
||||
return post("/api/knowledge-base/retrieve", data);
|
||||
}
|
||||
|
||||
@@ -1,150 +1,150 @@
|
||||
import {
|
||||
BookOpen,
|
||||
BookOpenText,
|
||||
BookType,
|
||||
ChartNoAxesColumn,
|
||||
CheckCircle,
|
||||
CircleEllipsis,
|
||||
Clock,
|
||||
Database,
|
||||
File,
|
||||
VectorSquare,
|
||||
XCircle,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
KBFile,
|
||||
KBFileStatus,
|
||||
KBType,
|
||||
KnowledgeBaseItem,
|
||||
} from "./knowledge-base.model";
|
||||
import { formatBytes, formatDateTime, formatNumber } from "@/utils/unit";
|
||||
|
||||
export const KBFileStatusMap = {
|
||||
[KBFileStatus.PROCESSED]: {
|
||||
value: KBFileStatus.PROCESSED,
|
||||
label: "已处理",
|
||||
icon: CheckCircle,
|
||||
color: "#389e0d",
|
||||
},
|
||||
[KBFileStatus.PROCESSING]: {
|
||||
value: KBFileStatus.PROCESSING,
|
||||
label: "处理中",
|
||||
icon: Clock,
|
||||
color: "#faad14",
|
||||
},
|
||||
[KBFileStatus.PROCESS_FAILED]: {
|
||||
value: KBFileStatus.PROCESS_FAILED,
|
||||
label: "处理失败",
|
||||
icon: XCircle,
|
||||
color: "#ff4d4f",
|
||||
},
|
||||
[KBFileStatus.UNPROCESSED]: {
|
||||
value: KBFileStatus.UNPROCESSED,
|
||||
label: "未处理",
|
||||
icon: CircleEllipsis,
|
||||
color: "#d9d9d9",
|
||||
},
|
||||
};
|
||||
|
||||
export const KBTypeMap = {
|
||||
[KBType.STRUCTURED]: {
|
||||
value: KBType.STRUCTURED,
|
||||
label: "结构化",
|
||||
icon: Database,
|
||||
iconColor: "blue",
|
||||
description: "用于处理和分析文本数据的数据集",
|
||||
},
|
||||
[KBType.UNSTRUCTURED]: {
|
||||
value: KBType.UNSTRUCTURED,
|
||||
label: "非结构化",
|
||||
icon: BookOpen,
|
||||
iconColor: "green",
|
||||
description: "适用于存储和管理各种格式的文件",
|
||||
},
|
||||
};
|
||||
|
||||
export function mapKnowledgeBase(
|
||||
kb: KnowledgeBaseItem,
|
||||
showModelFields: boolean = true
|
||||
): KnowledgeBaseItem {
|
||||
return {
|
||||
...kb,
|
||||
icon: <BookOpenText className="w-full h-full" />,
|
||||
description: kb.description,
|
||||
statistics: [
|
||||
...(showModelFields
|
||||
? [
|
||||
{
|
||||
label: "索引模型",
|
||||
key: "embeddingModel",
|
||||
icon: <VectorSquare className="w-4 h-4 text-blue-500" />,
|
||||
value:
|
||||
kb.embedding?.modelName +
|
||||
(kb.embedding?.provider
|
||||
? ` (${kb.embedding.provider})`
|
||||
: "") || "无",
|
||||
},
|
||||
{
|
||||
label: "文本理解模型",
|
||||
key: "chatModel",
|
||||
icon: <BookType className="w-4 h-4 text-blue-500" />,
|
||||
value:
|
||||
kb.chat?.modelName +
|
||||
(kb.chat?.provider ? ` (${kb.chat.provider})` : "") || "无",
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
label: "文件数",
|
||||
key: "fileCount",
|
||||
icon: <File className="w-4 h-4 text-blue-500" />,
|
||||
value: formatNumber(kb?.fileCount) || 0,
|
||||
},
|
||||
{
|
||||
label: "分块数",
|
||||
key: "chunkCount",
|
||||
icon: <ChartNoAxesColumn className="w-4 h-4 text-blue-500" />,
|
||||
value: formatNumber(kb?.chunkCount) || 0,
|
||||
},
|
||||
],
|
||||
updatedAt: formatDateTime(kb.updatedAt),
|
||||
createdAt: formatDateTime(kb.createdAt),
|
||||
};
|
||||
}
|
||||
|
||||
export function mapFileData(file: Partial<KBFile>): KBFile {
|
||||
return {
|
||||
...file,
|
||||
name: file.fileName,
|
||||
createdAt: formatDateTime(file.createdAt),
|
||||
updatedAt: formatDateTime(file.updatedAt),
|
||||
status: KBFileStatusMap[file.status] || {
|
||||
value: file.status,
|
||||
label: "未知状态",
|
||||
icon: CircleEllipsis,
|
||||
color: "#d9d9d9",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const DatasetFileCols = [
|
||||
{
|
||||
title: "所属数据集",
|
||||
dataIndex: "datasetName",
|
||||
key: "datasetName",
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: "文件名",
|
||||
dataIndex: "fileName",
|
||||
key: "fileName",
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: "大小",
|
||||
dataIndex: "fileSize",
|
||||
key: "fileSize",
|
||||
ellipsis: true,
|
||||
render: formatBytes,
|
||||
},
|
||||
];
|
||||
import {
|
||||
BookOpen,
|
||||
BookOpenText,
|
||||
BookType,
|
||||
ChartNoAxesColumn,
|
||||
CheckCircle,
|
||||
CircleEllipsis,
|
||||
Clock,
|
||||
Database,
|
||||
File,
|
||||
VectorSquare,
|
||||
XCircle,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
KBFile,
|
||||
KBFileStatus,
|
||||
KBType,
|
||||
KnowledgeBaseItem,
|
||||
} from "./knowledge-base.model";
|
||||
import { formatBytes, formatDateTime, formatNumber } from "@/utils/unit";
|
||||
|
||||
export const KBFileStatusMap = {
|
||||
[KBFileStatus.PROCESSED]: {
|
||||
value: KBFileStatus.PROCESSED,
|
||||
label: "已处理",
|
||||
icon: CheckCircle,
|
||||
color: "#389e0d",
|
||||
},
|
||||
[KBFileStatus.PROCESSING]: {
|
||||
value: KBFileStatus.PROCESSING,
|
||||
label: "处理中",
|
||||
icon: Clock,
|
||||
color: "#faad14",
|
||||
},
|
||||
[KBFileStatus.PROCESS_FAILED]: {
|
||||
value: KBFileStatus.PROCESS_FAILED,
|
||||
label: "处理失败",
|
||||
icon: XCircle,
|
||||
color: "#ff4d4f",
|
||||
},
|
||||
[KBFileStatus.UNPROCESSED]: {
|
||||
value: KBFileStatus.UNPROCESSED,
|
||||
label: "未处理",
|
||||
icon: CircleEllipsis,
|
||||
color: "#d9d9d9",
|
||||
},
|
||||
};
|
||||
|
||||
export const KBTypeMap = {
|
||||
[KBType.STRUCTURED]: {
|
||||
value: KBType.STRUCTURED,
|
||||
label: "结构化",
|
||||
icon: Database,
|
||||
iconColor: "blue",
|
||||
description: "用于处理和分析文本数据的数据集",
|
||||
},
|
||||
[KBType.UNSTRUCTURED]: {
|
||||
value: KBType.UNSTRUCTURED,
|
||||
label: "非结构化",
|
||||
icon: BookOpen,
|
||||
iconColor: "green",
|
||||
description: "适用于存储和管理各种格式的文件",
|
||||
},
|
||||
};
|
||||
|
||||
export function mapKnowledgeBase(
|
||||
kb: KnowledgeBaseItem,
|
||||
showModelFields: boolean = true
|
||||
): KnowledgeBaseItem {
|
||||
return {
|
||||
...kb,
|
||||
icon: <BookOpenText className="w-full h-full" />,
|
||||
description: kb.description,
|
||||
statistics: [
|
||||
...(showModelFields
|
||||
? [
|
||||
{
|
||||
label: "索引模型",
|
||||
key: "embeddingModel",
|
||||
icon: <VectorSquare className="w-4 h-4 text-blue-500" />,
|
||||
value:
|
||||
kb.embedding?.modelName +
|
||||
(kb.embedding?.provider
|
||||
? ` (${kb.embedding.provider})`
|
||||
: "") || "无",
|
||||
},
|
||||
{
|
||||
label: "文本理解模型",
|
||||
key: "chatModel",
|
||||
icon: <BookType className="w-4 h-4 text-blue-500" />,
|
||||
value:
|
||||
kb.chat?.modelName +
|
||||
(kb.chat?.provider ? ` (${kb.chat.provider})` : "") || "无",
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
label: "文件数",
|
||||
key: "fileCount",
|
||||
icon: <File className="w-4 h-4 text-blue-500" />,
|
||||
value: formatNumber(kb?.fileCount) || 0,
|
||||
},
|
||||
{
|
||||
label: "分块数",
|
||||
key: "chunkCount",
|
||||
icon: <ChartNoAxesColumn className="w-4 h-4 text-blue-500" />,
|
||||
value: formatNumber(kb?.chunkCount) || 0,
|
||||
},
|
||||
],
|
||||
updatedAt: formatDateTime(kb.updatedAt),
|
||||
createdAt: formatDateTime(kb.createdAt),
|
||||
};
|
||||
}
|
||||
|
||||
export function mapFileData(file: Partial<KBFile>): KBFile {
|
||||
return {
|
||||
...file,
|
||||
name: file.fileName,
|
||||
createdAt: formatDateTime(file.createdAt),
|
||||
updatedAt: formatDateTime(file.updatedAt),
|
||||
status: KBFileStatusMap[file.status] || {
|
||||
value: file.status,
|
||||
label: "未知状态",
|
||||
icon: CircleEllipsis,
|
||||
color: "#d9d9d9",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const DatasetFileCols = [
|
||||
{
|
||||
title: "所属数据集",
|
||||
dataIndex: "datasetName",
|
||||
key: "datasetName",
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: "文件名",
|
||||
dataIndex: "fileName",
|
||||
key: "fileName",
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: "大小",
|
||||
dataIndex: "fileSize",
|
||||
key: "fileSize",
|
||||
ellipsis: true,
|
||||
render: formatBytes,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,78 +1,78 @@
|
||||
export enum KBFileStatus {
|
||||
UNPROCESSED = "UNPROCESSED",
|
||||
PROCESSING = "PROCESSING",
|
||||
PROCESSED = "PROCESSED",
|
||||
PROCESS_FAILED = "PROCESS_FAILED",
|
||||
}
|
||||
|
||||
export enum KBType {
|
||||
UNSTRUCTURED = "unstructured",
|
||||
STRUCTURED = "structured",
|
||||
}
|
||||
|
||||
export interface KnowledgeBaseItem {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
type: KBType;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
embeddingModel: string;
|
||||
chatModel: string;
|
||||
fileCount: number;
|
||||
chunkCount: number;
|
||||
embedding: never;
|
||||
chat: never;
|
||||
}
|
||||
|
||||
export interface KBFile {
|
||||
id: string;
|
||||
fileName: string;
|
||||
name?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
status: KBFileStatus;
|
||||
chunkCount: number;
|
||||
metadata: Record<string, any>;
|
||||
knowledgeBaseId: string;
|
||||
fileId: string;
|
||||
updatedBy: string;
|
||||
createdBy: string;
|
||||
}
|
||||
|
||||
interface Chunk {
|
||||
id: number;
|
||||
content: string;
|
||||
position: number;
|
||||
tokens: number;
|
||||
embedding?: number[];
|
||||
similarity?: string;
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
vectorId?: string;
|
||||
sliceOperator?: string;
|
||||
parentChunkId?: number;
|
||||
metadata?: {
|
||||
source: string;
|
||||
page?: number;
|
||||
section?: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface VectorizationRecord {
|
||||
id: number;
|
||||
timestamp: string;
|
||||
operation: "create" | "update" | "delete" | "reprocess";
|
||||
fileId: number;
|
||||
fileName: string;
|
||||
chunksProcessed: number;
|
||||
vectorsGenerated: number;
|
||||
status: "success" | "failed" | "partial";
|
||||
duration: string;
|
||||
config: {
|
||||
embeddingModel: string;
|
||||
chunkSize: number;
|
||||
sliceMethod: string;
|
||||
};
|
||||
error?: string;
|
||||
}
|
||||
export enum KBFileStatus {
|
||||
UNPROCESSED = "UNPROCESSED",
|
||||
PROCESSING = "PROCESSING",
|
||||
PROCESSED = "PROCESSED",
|
||||
PROCESS_FAILED = "PROCESS_FAILED",
|
||||
}
|
||||
|
||||
export enum KBType {
|
||||
UNSTRUCTURED = "unstructured",
|
||||
STRUCTURED = "structured",
|
||||
}
|
||||
|
||||
export interface KnowledgeBaseItem {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
type: KBType;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
embeddingModel: string;
|
||||
chatModel: string;
|
||||
fileCount: number;
|
||||
chunkCount: number;
|
||||
embedding: never;
|
||||
chat: never;
|
||||
}
|
||||
|
||||
export interface KBFile {
|
||||
id: string;
|
||||
fileName: string;
|
||||
name?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
status: KBFileStatus;
|
||||
chunkCount: number;
|
||||
metadata: Record<string, any>;
|
||||
knowledgeBaseId: string;
|
||||
fileId: string;
|
||||
updatedBy: string;
|
||||
createdBy: string;
|
||||
}
|
||||
|
||||
interface Chunk {
|
||||
id: number;
|
||||
content: string;
|
||||
position: number;
|
||||
tokens: number;
|
||||
embedding?: number[];
|
||||
similarity?: string;
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
vectorId?: string;
|
||||
sliceOperator?: string;
|
||||
parentChunkId?: number;
|
||||
metadata?: {
|
||||
source: string;
|
||||
page?: number;
|
||||
section?: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface VectorizationRecord {
|
||||
id: number;
|
||||
timestamp: string;
|
||||
operation: "create" | "update" | "delete" | "reprocess";
|
||||
fileId: number;
|
||||
fileName: string;
|
||||
chunksProcessed: number;
|
||||
vectorsGenerated: number;
|
||||
status: "success" | "failed" | "partial";
|
||||
duration: string;
|
||||
config: {
|
||||
embeddingModel: string;
|
||||
chunkSize: number;
|
||||
sliceMethod: string;
|
||||
};
|
||||
error?: string;
|
||||
}
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
import React, { memo } from "react";
|
||||
import { Outlet } from "react-router";
|
||||
import Sidebar from "./Sidebar";
|
||||
|
||||
const MainLayout = () => {
|
||||
return (
|
||||
<div className="w-full h-screen flex flex-col bg-gray-50 min-w-6xl">
|
||||
<div className="w-full h-full flex">
|
||||
{/* Sidebar */}
|
||||
<Sidebar />
|
||||
{/* Main Content */}
|
||||
<div className="flex-overflow-auto p-6">
|
||||
{/* Content Area */}
|
||||
<div className="flex-overflow-auto">
|
||||
<Outlet />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(MainLayout);
|
||||
import React, { memo } from "react";
|
||||
import { Outlet } from "react-router";
|
||||
import Sidebar from "./Sidebar";
|
||||
|
||||
const MainLayout = () => {
|
||||
return (
|
||||
<div className="w-full h-screen flex flex-col bg-gray-50 min-w-6xl">
|
||||
<div className="w-full h-full flex">
|
||||
{/* Sidebar */}
|
||||
<Sidebar />
|
||||
{/* Main Content */}
|
||||
<div className="flex-overflow-auto p-6">
|
||||
{/* Content Area */}
|
||||
<div className="flex-overflow-auto">
|
||||
<Outlet />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(MainLayout);
|
||||
|
||||
@@ -1,206 +1,206 @@
|
||||
import { memo, useEffect, useState } from "react";
|
||||
import { Button, Drawer, Menu, Popover } from "antd";
|
||||
import {
|
||||
CloseOutlined,
|
||||
MenuOutlined,
|
||||
SettingOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { ClipboardList, Sparkles, X } from "lucide-react";
|
||||
import { menuItems } from "@/pages/Layout/menu";
|
||||
import { NavLink, useLocation, useNavigate } from "react-router";
|
||||
import TaskUpload from "./TaskUpload";
|
||||
import SettingsPage from "../SettingsPage/SettingsPage";
|
||||
import { useAppSelector, useAppDispatch } from "@/store/hooks";
|
||||
import { showSettings, hideSettings } from "@/store/slices/settingsSlice";
|
||||
|
||||
const AsiderAndHeaderLayout = () => {
|
||||
const { pathname } = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const [activeItem, setActiveItem] = useState<string>("");
|
||||
const [sidebarOpen, setSidebarOpen] = useState(true);
|
||||
const [taskCenterVisible, setTaskCenterVisible] = useState(false);
|
||||
const settingVisible = useAppSelector((state) => state.settings.visible);
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
// Initialize active item based on current pathname
|
||||
const initActiveItem = () => {
|
||||
for (let index = 0; index < menuItems.length; index++) {
|
||||
const element = menuItems[index];
|
||||
if (element.children) {
|
||||
element.children.forEach((subItem) => {
|
||||
if (pathname.includes(subItem.id)) {
|
||||
setActiveItem(subItem.id);
|
||||
return;
|
||||
}
|
||||
});
|
||||
} else if (pathname.includes(element.id)) {
|
||||
setActiveItem(element.id);
|
||||
return;
|
||||
}
|
||||
}
|
||||
console.log(pathname);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
initActiveItem();
|
||||
}, [pathname]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleShowTaskPopover = (event: CustomEvent) => {
|
||||
const { show } = event.detail;
|
||||
setTaskCenterVisible(show);
|
||||
};
|
||||
|
||||
window.addEventListener(
|
||||
"show:task-popover",
|
||||
handleShowTaskPopover as EventListener
|
||||
);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener(
|
||||
"show:task-popover",
|
||||
handleShowTaskPopover as EventListener
|
||||
);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`${
|
||||
sidebarOpen ? "w-64" : "w-20"
|
||||
} bg-white border-r border-gray-200 transition-all duration-300 flex flex-col relative`}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-4 border-b border-gray-200">
|
||||
{sidebarOpen && (
|
||||
<NavLink to="/" className="flex items-center gap-2 cursor-pointer">
|
||||
<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" />
|
||||
</div>
|
||||
<span className="text-lg font-bold text-gray-900">DataMate</span>
|
||||
</NavLink>
|
||||
)}
|
||||
<span
|
||||
className="cursor-pointer hover:text-blue-500"
|
||||
onClick={() => setSidebarOpen(!sidebarOpen)}
|
||||
>
|
||||
{sidebarOpen ? <CloseOutlined /> : <MenuOutlined className="ml-4" />}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Navigation */}
|
||||
<div className="flex-1">
|
||||
<Menu
|
||||
mode="inline"
|
||||
inlineCollapsed={!sidebarOpen}
|
||||
items={menuItems.map((item) => ({
|
||||
key: item.id,
|
||||
label: item.title,
|
||||
icon: item.icon ? <item.icon className="w-4 h-4" /> : null,
|
||||
children: item.children
|
||||
? item.children.map((subItem) => ({
|
||||
key: subItem.id,
|
||||
label: subItem.title,
|
||||
icon: subItem.icon ? (
|
||||
<subItem.icon className="w-4 h-4" />
|
||||
) : null,
|
||||
}))
|
||||
: undefined,
|
||||
}))}
|
||||
selectedKeys={[activeItem]}
|
||||
defaultOpenKeys={["synthesis"]}
|
||||
onClick={({ key }) => {
|
||||
setActiveItem(key);
|
||||
navigate(`/data/${key}`);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="p-4 border-t border-gray-200">
|
||||
{sidebarOpen ? (
|
||||
<div className="space-y-2">
|
||||
<Popover
|
||||
forceRender
|
||||
title={
|
||||
<div className="flex items-center justify-between gap-2 border-b border-gray-200 pb-2 mb-2">
|
||||
<h4 className="font-bold">任务中心</h4>
|
||||
<X
|
||||
onClick={() => setTaskCenterVisible(false)}
|
||||
className="cursor-pointer w-4 h-4 text-gray-500 hover:text-gray-900"
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
open={taskCenterVisible}
|
||||
content={<TaskUpload />}
|
||||
trigger="click"
|
||||
destroyOnHidden={false}
|
||||
>
|
||||
<Button block onClick={() => setTaskCenterVisible(true)}>
|
||||
任务中心
|
||||
</Button>
|
||||
</Popover>
|
||||
<Button
|
||||
block
|
||||
onClick={() => {
|
||||
dispatch(showSettings());
|
||||
}}
|
||||
>
|
||||
设置
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
<div className="relative">
|
||||
<Popover
|
||||
forceRender
|
||||
title="任务中心"
|
||||
open={taskCenterVisible}
|
||||
content={<TaskUpload />}
|
||||
trigger="click"
|
||||
destroyOnHidden={false}
|
||||
>
|
||||
<Button
|
||||
block
|
||||
onClick={() => setTaskCenterVisible(true)}
|
||||
icon={<ClipboardList className="w-4 h-4" />}
|
||||
></Button>
|
||||
</Popover>
|
||||
</div>
|
||||
<Button
|
||||
block
|
||||
onClick={() => {
|
||||
dispatch(showSettings());
|
||||
}}
|
||||
>
|
||||
<SettingOutlined />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Drawer
|
||||
title="设置"
|
||||
placement="bottom"
|
||||
width="100%"
|
||||
height="100%"
|
||||
open={settingVisible}
|
||||
onClose={() => dispatch(hideSettings())}
|
||||
bodyStyle={{ padding: 0 }}
|
||||
destroyOnHidden={true}
|
||||
>
|
||||
<SettingsPage></SettingsPage>
|
||||
</Drawer>
|
||||
{/* 添加遮罩层,点击外部区域时关闭 */}
|
||||
{taskCenterVisible && (
|
||||
<div
|
||||
className="fixed inset-0 z-40"
|
||||
onClick={() => {
|
||||
setTaskCenterVisible(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(AsiderAndHeaderLayout);
|
||||
import { memo, useEffect, useState } from "react";
|
||||
import { Button, Drawer, Menu, Popover } from "antd";
|
||||
import {
|
||||
CloseOutlined,
|
||||
MenuOutlined,
|
||||
SettingOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { ClipboardList, Sparkles, X } from "lucide-react";
|
||||
import { menuItems } from "@/pages/Layout/menu";
|
||||
import { NavLink, useLocation, useNavigate } from "react-router";
|
||||
import TaskUpload from "./TaskUpload";
|
||||
import SettingsPage from "../SettingsPage/SettingsPage";
|
||||
import { useAppSelector, useAppDispatch } from "@/store/hooks";
|
||||
import { showSettings, hideSettings } from "@/store/slices/settingsSlice";
|
||||
|
||||
const AsiderAndHeaderLayout = () => {
|
||||
const { pathname } = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const [activeItem, setActiveItem] = useState<string>("");
|
||||
const [sidebarOpen, setSidebarOpen] = useState(true);
|
||||
const [taskCenterVisible, setTaskCenterVisible] = useState(false);
|
||||
const settingVisible = useAppSelector((state) => state.settings.visible);
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
// Initialize active item based on current pathname
|
||||
const initActiveItem = () => {
|
||||
for (let index = 0; index < menuItems.length; index++) {
|
||||
const element = menuItems[index];
|
||||
if (element.children) {
|
||||
element.children.forEach((subItem) => {
|
||||
if (pathname.includes(subItem.id)) {
|
||||
setActiveItem(subItem.id);
|
||||
return;
|
||||
}
|
||||
});
|
||||
} else if (pathname.includes(element.id)) {
|
||||
setActiveItem(element.id);
|
||||
return;
|
||||
}
|
||||
}
|
||||
console.log(pathname);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
initActiveItem();
|
||||
}, [pathname]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleShowTaskPopover = (event: CustomEvent) => {
|
||||
const { show } = event.detail;
|
||||
setTaskCenterVisible(show);
|
||||
};
|
||||
|
||||
window.addEventListener(
|
||||
"show:task-popover",
|
||||
handleShowTaskPopover as EventListener
|
||||
);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener(
|
||||
"show:task-popover",
|
||||
handleShowTaskPopover as EventListener
|
||||
);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`${
|
||||
sidebarOpen ? "w-64" : "w-20"
|
||||
} bg-white border-r border-gray-200 transition-all duration-300 flex flex-col relative`}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-4 border-b border-gray-200">
|
||||
{sidebarOpen && (
|
||||
<NavLink to="/" className="flex items-center gap-2 cursor-pointer">
|
||||
<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" />
|
||||
</div>
|
||||
<span className="text-lg font-bold text-gray-900">DataMate</span>
|
||||
</NavLink>
|
||||
)}
|
||||
<span
|
||||
className="cursor-pointer hover:text-blue-500"
|
||||
onClick={() => setSidebarOpen(!sidebarOpen)}
|
||||
>
|
||||
{sidebarOpen ? <CloseOutlined /> : <MenuOutlined className="ml-4" />}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Navigation */}
|
||||
<div className="flex-1">
|
||||
<Menu
|
||||
mode="inline"
|
||||
inlineCollapsed={!sidebarOpen}
|
||||
items={menuItems.map((item) => ({
|
||||
key: item.id,
|
||||
label: item.title,
|
||||
icon: item.icon ? <item.icon className="w-4 h-4" /> : null,
|
||||
children: item.children
|
||||
? item.children.map((subItem) => ({
|
||||
key: subItem.id,
|
||||
label: subItem.title,
|
||||
icon: subItem.icon ? (
|
||||
<subItem.icon className="w-4 h-4" />
|
||||
) : null,
|
||||
}))
|
||||
: undefined,
|
||||
}))}
|
||||
selectedKeys={[activeItem]}
|
||||
defaultOpenKeys={["synthesis"]}
|
||||
onClick={({ key }) => {
|
||||
setActiveItem(key);
|
||||
navigate(`/data/${key}`);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="p-4 border-t border-gray-200">
|
||||
{sidebarOpen ? (
|
||||
<div className="space-y-2">
|
||||
<Popover
|
||||
forceRender
|
||||
title={
|
||||
<div className="flex items-center justify-between gap-2 border-b border-gray-200 pb-2 mb-2">
|
||||
<h4 className="font-bold">任务中心</h4>
|
||||
<X
|
||||
onClick={() => setTaskCenterVisible(false)}
|
||||
className="cursor-pointer w-4 h-4 text-gray-500 hover:text-gray-900"
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
open={taskCenterVisible}
|
||||
content={<TaskUpload />}
|
||||
trigger="click"
|
||||
destroyOnHidden={false}
|
||||
>
|
||||
<Button block onClick={() => setTaskCenterVisible(true)}>
|
||||
任务中心
|
||||
</Button>
|
||||
</Popover>
|
||||
<Button
|
||||
block
|
||||
onClick={() => {
|
||||
dispatch(showSettings());
|
||||
}}
|
||||
>
|
||||
设置
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
<div className="relative">
|
||||
<Popover
|
||||
forceRender
|
||||
title="任务中心"
|
||||
open={taskCenterVisible}
|
||||
content={<TaskUpload />}
|
||||
trigger="click"
|
||||
destroyOnHidden={false}
|
||||
>
|
||||
<Button
|
||||
block
|
||||
onClick={() => setTaskCenterVisible(true)}
|
||||
icon={<ClipboardList className="w-4 h-4" />}
|
||||
></Button>
|
||||
</Popover>
|
||||
</div>
|
||||
<Button
|
||||
block
|
||||
onClick={() => {
|
||||
dispatch(showSettings());
|
||||
}}
|
||||
>
|
||||
<SettingOutlined />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Drawer
|
||||
title="设置"
|
||||
placement="bottom"
|
||||
width="100%"
|
||||
height="100%"
|
||||
open={settingVisible}
|
||||
onClose={() => dispatch(hideSettings())}
|
||||
bodyStyle={{ padding: 0 }}
|
||||
destroyOnHidden={true}
|
||||
>
|
||||
<SettingsPage></SettingsPage>
|
||||
</Drawer>
|
||||
{/* 添加遮罩层,点击外部区域时关闭 */}
|
||||
{taskCenterVisible && (
|
||||
<div
|
||||
className="fixed inset-0 z-40"
|
||||
onClick={() => {
|
||||
setTaskCenterVisible(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(AsiderAndHeaderLayout);
|
||||
|
||||
@@ -1,67 +1,67 @@
|
||||
import {
|
||||
cancelUploadUsingPut,
|
||||
preUploadUsingPost,
|
||||
uploadFileChunkUsingPost,
|
||||
} from "@/pages/DataManagement/dataset.api";
|
||||
import { Button, Empty, Progress } from "antd";
|
||||
import { DeleteOutlined } from "@ant-design/icons";
|
||||
import { useEffect } from "react";
|
||||
import { useFileSliceUpload } from "@/hooks/useSliceUpload";
|
||||
|
||||
export default function TaskUpload() {
|
||||
const { createTask, taskList, removeTask, handleUpload } = useFileSliceUpload(
|
||||
{
|
||||
preUpload: preUploadUsingPost,
|
||||
uploadChunk: uploadFileChunkUsingPost,
|
||||
cancelUpload: cancelUploadUsingPut,
|
||||
}
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const uploadHandler = (e: any) => {
|
||||
const { files } = e.detail;
|
||||
const task = createTask(e.detail);
|
||||
handleUpload({ task, files });
|
||||
};
|
||||
window.addEventListener("upload:dataset", uploadHandler);
|
||||
return () => {
|
||||
window.removeEventListener("upload:dataset", uploadHandler);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="w-90 max-w-90 max-h-96 overflow-y-auto p-2"
|
||||
id="header-task-popover"
|
||||
>
|
||||
{taskList.length > 0 &&
|
||||
taskList.map((task) => (
|
||||
<div key={task.key} className="border-b border-gray-200 pb-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>{task.title}</div>
|
||||
<Button
|
||||
type="text"
|
||||
danger
|
||||
disabled={!task?.cancelFn}
|
||||
onClick={() =>
|
||||
removeTask({
|
||||
...task,
|
||||
isCancel: true,
|
||||
})
|
||||
}
|
||||
icon={<DeleteOutlined />}
|
||||
></Button>
|
||||
</div>
|
||||
|
||||
<Progress size="small" percent={task.percent} />
|
||||
</div>
|
||||
))}
|
||||
{taskList.length === 0 && (
|
||||
<Empty
|
||||
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
||||
description="暂无上传任务"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
import {
|
||||
cancelUploadUsingPut,
|
||||
preUploadUsingPost,
|
||||
uploadFileChunkUsingPost,
|
||||
} from "@/pages/DataManagement/dataset.api";
|
||||
import { Button, Empty, Progress } from "antd";
|
||||
import { DeleteOutlined } from "@ant-design/icons";
|
||||
import { useEffect } from "react";
|
||||
import { useFileSliceUpload } from "@/hooks/useSliceUpload";
|
||||
|
||||
export default function TaskUpload() {
|
||||
const { createTask, taskList, removeTask, handleUpload } = useFileSliceUpload(
|
||||
{
|
||||
preUpload: preUploadUsingPost,
|
||||
uploadChunk: uploadFileChunkUsingPost,
|
||||
cancelUpload: cancelUploadUsingPut,
|
||||
}
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const uploadHandler = (e: any) => {
|
||||
const { files } = e.detail;
|
||||
const task = createTask(e.detail);
|
||||
handleUpload({ task, files });
|
||||
};
|
||||
window.addEventListener("upload:dataset", uploadHandler);
|
||||
return () => {
|
||||
window.removeEventListener("upload:dataset", uploadHandler);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="w-90 max-w-90 max-h-96 overflow-y-auto p-2"
|
||||
id="header-task-popover"
|
||||
>
|
||||
{taskList.length > 0 &&
|
||||
taskList.map((task) => (
|
||||
<div key={task.key} className="border-b border-gray-200 pb-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>{task.title}</div>
|
||||
<Button
|
||||
type="text"
|
||||
danger
|
||||
disabled={!task?.cancelFn}
|
||||
onClick={() =>
|
||||
removeTask({
|
||||
...task,
|
||||
isCancel: true,
|
||||
})
|
||||
}
|
||||
icon={<DeleteOutlined />}
|
||||
></Button>
|
||||
</div>
|
||||
|
||||
<Progress size="small" percent={task.percent} />
|
||||
</div>
|
||||
))}
|
||||
{taskList.length === 0 && (
|
||||
<Empty
|
||||
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
||||
description="暂无上传任务"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,115 +1,115 @@
|
||||
import {
|
||||
FolderOpen,
|
||||
Tag,
|
||||
Target,
|
||||
BookOpen,
|
||||
Shuffle,
|
||||
BarChart3,
|
||||
MessageSquare,
|
||||
GitBranch,
|
||||
Zap,
|
||||
Shield,
|
||||
Database,
|
||||
Store,
|
||||
Merge,
|
||||
} from "lucide-react";
|
||||
|
||||
export const menuItems = [
|
||||
{
|
||||
id: "collection",
|
||||
title: "数据归集",
|
||||
icon: Database,
|
||||
description: "创建、导入和管理数据集",
|
||||
color: "bg-orange-500",
|
||||
},
|
||||
{
|
||||
id: "management",
|
||||
title: "数据管理",
|
||||
icon: FolderOpen,
|
||||
description: "创建、导入和管理数据集",
|
||||
color: "bg-blue-500",
|
||||
},
|
||||
{
|
||||
id: "cleansing",
|
||||
title: "数据清洗",
|
||||
icon: GitBranch,
|
||||
description: "数据清洗和预处理",
|
||||
color: "bg-purple-500",
|
||||
},
|
||||
{
|
||||
id: "annotation",
|
||||
title: "数据标注",
|
||||
icon: Tag,
|
||||
description: "对数据进行标注和标记",
|
||||
color: "bg-green-500",
|
||||
},
|
||||
{
|
||||
id: "synthesis",
|
||||
title: "数据合成",
|
||||
icon: Shuffle,
|
||||
description: "智能数据合成和配比",
|
||||
color: "bg-pink-500",
|
||||
children: [
|
||||
{
|
||||
id: "synthesis/task",
|
||||
title: "合成任务",
|
||||
icon: Merge,
|
||||
},
|
||||
{
|
||||
id: "synthesis/ratio-task",
|
||||
title: "配比任务",
|
||||
icon: BarChart3,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "evaluation",
|
||||
title: "数据评估",
|
||||
icon: Target,
|
||||
badge: 4,
|
||||
description: "质量分析、性能评估和偏见检测",
|
||||
color: "bg-indigo-500",
|
||||
},
|
||||
{
|
||||
id: "knowledge-base",
|
||||
title: "知识生成",
|
||||
icon: BookOpen,
|
||||
description: "面向RAG的知识库构建",
|
||||
color: "bg-teal-500",
|
||||
},
|
||||
{
|
||||
id: "operator-market",
|
||||
title: "算子市场",
|
||||
icon: Store,
|
||||
description: "算子上传与管理",
|
||||
color: "bg-yellow-500",
|
||||
},
|
||||
];
|
||||
|
||||
export const features = [
|
||||
{
|
||||
icon: GitBranch,
|
||||
title: "智能编排",
|
||||
description: "可视化数据清洗流程编排,拖拽式设计复杂的数据清洗管道",
|
||||
},
|
||||
{
|
||||
icon: MessageSquare,
|
||||
title: "对话助手",
|
||||
description: "通过自然语言对话完成复杂的数据集操作和业务流程",
|
||||
},
|
||||
{
|
||||
icon: Target,
|
||||
title: "全面评估",
|
||||
description: "多维度数据质量评估,包含统计分析、性能测试和偏见检测",
|
||||
},
|
||||
{
|
||||
icon: Zap,
|
||||
title: "高效处理",
|
||||
description: "完整的数据清洗流水线,从原始数据到可用数据集",
|
||||
},
|
||||
{
|
||||
icon: Shield,
|
||||
title: "知识管理",
|
||||
description: "构建面向RAG的知识库,支持智能问答和检索",
|
||||
},
|
||||
];
|
||||
import {
|
||||
FolderOpen,
|
||||
Tag,
|
||||
Target,
|
||||
BookOpen,
|
||||
Shuffle,
|
||||
BarChart3,
|
||||
MessageSquare,
|
||||
GitBranch,
|
||||
Zap,
|
||||
Shield,
|
||||
Database,
|
||||
Store,
|
||||
Merge,
|
||||
} from "lucide-react";
|
||||
|
||||
export const menuItems = [
|
||||
{
|
||||
id: "collection",
|
||||
title: "数据归集",
|
||||
icon: Database,
|
||||
description: "创建、导入和管理数据集",
|
||||
color: "bg-orange-500",
|
||||
},
|
||||
{
|
||||
id: "management",
|
||||
title: "数据管理",
|
||||
icon: FolderOpen,
|
||||
description: "创建、导入和管理数据集",
|
||||
color: "bg-blue-500",
|
||||
},
|
||||
{
|
||||
id: "cleansing",
|
||||
title: "数据清洗",
|
||||
icon: GitBranch,
|
||||
description: "数据清洗和预处理",
|
||||
color: "bg-purple-500",
|
||||
},
|
||||
{
|
||||
id: "annotation",
|
||||
title: "数据标注",
|
||||
icon: Tag,
|
||||
description: "对数据进行标注和标记",
|
||||
color: "bg-green-500",
|
||||
},
|
||||
{
|
||||
id: "synthesis",
|
||||
title: "数据合成",
|
||||
icon: Shuffle,
|
||||
description: "智能数据合成和配比",
|
||||
color: "bg-pink-500",
|
||||
children: [
|
||||
{
|
||||
id: "synthesis/task",
|
||||
title: "合成任务",
|
||||
icon: Merge,
|
||||
},
|
||||
{
|
||||
id: "synthesis/ratio-task",
|
||||
title: "配比任务",
|
||||
icon: BarChart3,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "evaluation",
|
||||
title: "数据评估",
|
||||
icon: Target,
|
||||
badge: 4,
|
||||
description: "质量分析、性能评估和偏见检测",
|
||||
color: "bg-indigo-500",
|
||||
},
|
||||
{
|
||||
id: "knowledge-base",
|
||||
title: "知识生成",
|
||||
icon: BookOpen,
|
||||
description: "面向RAG的知识库构建",
|
||||
color: "bg-teal-500",
|
||||
},
|
||||
{
|
||||
id: "operator-market",
|
||||
title: "算子市场",
|
||||
icon: Store,
|
||||
description: "算子上传与管理",
|
||||
color: "bg-yellow-500",
|
||||
},
|
||||
];
|
||||
|
||||
export const features = [
|
||||
{
|
||||
icon: GitBranch,
|
||||
title: "智能编排",
|
||||
description: "可视化数据清洗流程编排,拖拽式设计复杂的数据清洗管道",
|
||||
},
|
||||
{
|
||||
icon: MessageSquare,
|
||||
title: "对话助手",
|
||||
description: "通过自然语言对话完成复杂的数据集操作和业务流程",
|
||||
},
|
||||
{
|
||||
icon: Target,
|
||||
title: "全面评估",
|
||||
description: "多维度数据质量评估,包含统计分析、性能测试和偏见检测",
|
||||
},
|
||||
{
|
||||
icon: Zap,
|
||||
title: "高效处理",
|
||||
description: "完整的数据清洗流水线,从原始数据到可用数据集",
|
||||
},
|
||||
{
|
||||
icon: Shield,
|
||||
title: "知识管理",
|
||||
description: "构建面向RAG的知识库,支持智能问答和检索",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,201 +1,201 @@
|
||||
import { Button, App, Steps } from "antd";
|
||||
import {
|
||||
ArrowLeft,
|
||||
CheckCircle,
|
||||
Settings,
|
||||
TagIcon,
|
||||
Upload,
|
||||
} from "lucide-react";
|
||||
import { useNavigate, useParams } from "react-router";
|
||||
import { useEffect, useState } from "react";
|
||||
import UploadStep from "./components/UploadStep";
|
||||
import ParsingStep from "./components/ParsingStep";
|
||||
import ConfigureStep from "./components/ConfigureStep";
|
||||
import PreviewStep from "./components/PreviewStep";
|
||||
import { useFileSliceUpload } from "@/hooks/useSliceUpload";
|
||||
import {
|
||||
createOperatorUsingPost,
|
||||
preUploadOperatorUsingPost,
|
||||
queryOperatorByIdUsingGet,
|
||||
updateOperatorByIdUsingPut,
|
||||
uploadOperatorChunkUsingPost,
|
||||
uploadOperatorUsingPost,
|
||||
} from "../operator.api";
|
||||
import { sliceFile } from "@/utils/file.util";
|
||||
|
||||
export default function OperatorPluginCreate() {
|
||||
const navigate = useNavigate();
|
||||
const { id } = useParams();
|
||||
const { message } = App.useApp();
|
||||
const [uploadStep, setUploadStep] = useState<
|
||||
"upload" | "parsing" | "configure" | "preview"
|
||||
>(id ? "configure" : "upload");
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
const [parsedInfo, setParsedInfo] = useState({});
|
||||
const [parseError, setParseError] = useState<string | null>(null);
|
||||
|
||||
const { handleUpload, createTask, taskList } = useFileSliceUpload(
|
||||
{
|
||||
preUpload: preUploadOperatorUsingPost,
|
||||
uploadChunk: uploadOperatorChunkUsingPost,
|
||||
cancelUpload: null,
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
// 模拟文件上传
|
||||
const handleFileUpload = async (files: FileList) => {
|
||||
setIsUploading(true);
|
||||
setParseError(null);
|
||||
try {
|
||||
const fileName = files[0].name;
|
||||
await handleUpload({
|
||||
task: createTask({
|
||||
dataset: { id: "operator-upload", name: "上传算子" },
|
||||
}),
|
||||
files: [
|
||||
{
|
||||
originFile: files[0],
|
||||
slices: sliceFile(files[0]),
|
||||
name: fileName,
|
||||
size: files[0].size,
|
||||
},
|
||||
], // 假设只上传一个文件
|
||||
});
|
||||
setParsedInfo({ ...parsedInfo, percent: 100 }); // 上传完成,进度100%
|
||||
// 解析文件过程
|
||||
const res = await uploadOperatorUsingPost({ fileName });
|
||||
const configs = res.data.settings && typeof res.data.settings === "string"
|
||||
? JSON.parse(res.data.settings)
|
||||
: {};
|
||||
const defaultParams: Record<string, string> = {};
|
||||
Object.keys(configs).forEach((key) => {
|
||||
const { value } = configs[key];
|
||||
defaultParams[key] = value;
|
||||
});
|
||||
setParsedInfo({ ...res.data, fileName, configs, defaultParams});
|
||||
setUploadStep("parsing");
|
||||
} catch (err) {
|
||||
setParseError("文件解析失败," + err.data.message);
|
||||
} finally {
|
||||
setIsUploading(false);
|
||||
setUploadStep("configure");
|
||||
}
|
||||
};
|
||||
|
||||
const handlePublish = async () => {
|
||||
try {
|
||||
if (id) {
|
||||
await updateOperatorByIdUsingPut(id, parsedInfo!);
|
||||
} else {
|
||||
await createOperatorUsingPost(parsedInfo);
|
||||
}
|
||||
setUploadStep("preview");
|
||||
} catch (err) {
|
||||
message.error("算子发布失败," + err.data.message);
|
||||
}
|
||||
};
|
||||
|
||||
const onFetchOperator = async (operatorId: string) => {
|
||||
// 编辑模式,加载已有算子信息逻辑待实现
|
||||
const { data } = await queryOperatorByIdUsingGet(operatorId);
|
||||
const configs = data.settings && typeof data.settings === "string"
|
||||
? JSON.parse(data.settings)
|
||||
: {};
|
||||
const defaultParams: Record<string, string> = {};
|
||||
Object.keys(configs).forEach((key) => {
|
||||
const { value } = configs[key];
|
||||
defaultParams[key] = value;
|
||||
});
|
||||
setParsedInfo({ ...data, configs, defaultParams});
|
||||
setUploadStep("configure");
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (id) {
|
||||
// 编辑模式,加载已有算子信息逻辑待实现
|
||||
onFetchOperator(id);
|
||||
}
|
||||
}, [id]);
|
||||
|
||||
return (
|
||||
<div className="flex-overflow-auto bg-gray-50">
|
||||
{/* Header */}
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex items-center gap-4">
|
||||
<Button type="text" onClick={() => navigate("/data/operator-market")}>
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
</Button>
|
||||
<h1 className="text-xl font-bold text-gray-900">
|
||||
{id ? "更新算子" : "上传算子"}
|
||||
</h1>
|
||||
</div>
|
||||
<div className="w-1/2">
|
||||
<Steps
|
||||
size="small"
|
||||
items={[
|
||||
{
|
||||
title: "上传文件",
|
||||
icon: <Upload />,
|
||||
},
|
||||
{
|
||||
title: "解析文件",
|
||||
icon: <Settings />,
|
||||
},
|
||||
{
|
||||
title: "配置信息",
|
||||
icon: <TagIcon />,
|
||||
},
|
||||
{
|
||||
title: "发布完成",
|
||||
icon: <CheckCircle />,
|
||||
},
|
||||
]}
|
||||
current={
|
||||
uploadStep === "upload"
|
||||
? 0
|
||||
: uploadStep === "parsing"
|
||||
? 1
|
||||
: uploadStep === "configure"
|
||||
? 2
|
||||
: 3
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-overflow-auto p-6 mt-4 bg-white border-card">
|
||||
<div className="flex-overflow-auto">
|
||||
{uploadStep === "upload" && (
|
||||
<UploadStep onUpload={handleFileUpload} isUploading={isUploading} />
|
||||
)}
|
||||
{uploadStep === "parsing" && (
|
||||
<ParsingStep
|
||||
parseProgress={taskList[0]?.percent || parsedInfo.percent || 0}
|
||||
uploadedFiles={taskList}
|
||||
/>
|
||||
)}
|
||||
{uploadStep === "configure" && (
|
||||
<ConfigureStep
|
||||
setParsedInfo={setParsedInfo}
|
||||
parseError={parseError}
|
||||
parsedInfo={parsedInfo}
|
||||
/>
|
||||
)}
|
||||
{uploadStep === "preview" && (
|
||||
<PreviewStep setUploadStep={setUploadStep} />
|
||||
)}
|
||||
</div>
|
||||
{uploadStep === "configure" && (
|
||||
<div className="flex justify-end gap-3 mt-8">
|
||||
<Button onClick={() => setUploadStep("upload")}>重新上传</Button>
|
||||
<Button type="primary" onClick={handlePublish}>
|
||||
{id ? "更新" : "发布"}算子
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
import { Button, App, Steps } from "antd";
|
||||
import {
|
||||
ArrowLeft,
|
||||
CheckCircle,
|
||||
Settings,
|
||||
TagIcon,
|
||||
Upload,
|
||||
} from "lucide-react";
|
||||
import { useNavigate, useParams } from "react-router";
|
||||
import { useEffect, useState } from "react";
|
||||
import UploadStep from "./components/UploadStep";
|
||||
import ParsingStep from "./components/ParsingStep";
|
||||
import ConfigureStep from "./components/ConfigureStep";
|
||||
import PreviewStep from "./components/PreviewStep";
|
||||
import { useFileSliceUpload } from "@/hooks/useSliceUpload";
|
||||
import {
|
||||
createOperatorUsingPost,
|
||||
preUploadOperatorUsingPost,
|
||||
queryOperatorByIdUsingGet,
|
||||
updateOperatorByIdUsingPut,
|
||||
uploadOperatorChunkUsingPost,
|
||||
uploadOperatorUsingPost,
|
||||
} from "../operator.api";
|
||||
import { sliceFile } from "@/utils/file.util";
|
||||
|
||||
export default function OperatorPluginCreate() {
|
||||
const navigate = useNavigate();
|
||||
const { id } = useParams();
|
||||
const { message } = App.useApp();
|
||||
const [uploadStep, setUploadStep] = useState<
|
||||
"upload" | "parsing" | "configure" | "preview"
|
||||
>(id ? "configure" : "upload");
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
const [parsedInfo, setParsedInfo] = useState({});
|
||||
const [parseError, setParseError] = useState<string | null>(null);
|
||||
|
||||
const { handleUpload, createTask, taskList } = useFileSliceUpload(
|
||||
{
|
||||
preUpload: preUploadOperatorUsingPost,
|
||||
uploadChunk: uploadOperatorChunkUsingPost,
|
||||
cancelUpload: null,
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
// 模拟文件上传
|
||||
const handleFileUpload = async (files: FileList) => {
|
||||
setIsUploading(true);
|
||||
setParseError(null);
|
||||
try {
|
||||
const fileName = files[0].name;
|
||||
await handleUpload({
|
||||
task: createTask({
|
||||
dataset: { id: "operator-upload", name: "上传算子" },
|
||||
}),
|
||||
files: [
|
||||
{
|
||||
originFile: files[0],
|
||||
slices: sliceFile(files[0]),
|
||||
name: fileName,
|
||||
size: files[0].size,
|
||||
},
|
||||
], // 假设只上传一个文件
|
||||
});
|
||||
setParsedInfo({ ...parsedInfo, percent: 100 }); // 上传完成,进度100%
|
||||
// 解析文件过程
|
||||
const res = await uploadOperatorUsingPost({ fileName });
|
||||
const configs = res.data.settings && typeof res.data.settings === "string"
|
||||
? JSON.parse(res.data.settings)
|
||||
: {};
|
||||
const defaultParams: Record<string, string> = {};
|
||||
Object.keys(configs).forEach((key) => {
|
||||
const { value } = configs[key];
|
||||
defaultParams[key] = value;
|
||||
});
|
||||
setParsedInfo({ ...res.data, fileName, configs, defaultParams});
|
||||
setUploadStep("parsing");
|
||||
} catch (err) {
|
||||
setParseError("文件解析失败," + err.data.message);
|
||||
} finally {
|
||||
setIsUploading(false);
|
||||
setUploadStep("configure");
|
||||
}
|
||||
};
|
||||
|
||||
const handlePublish = async () => {
|
||||
try {
|
||||
if (id) {
|
||||
await updateOperatorByIdUsingPut(id, parsedInfo!);
|
||||
} else {
|
||||
await createOperatorUsingPost(parsedInfo);
|
||||
}
|
||||
setUploadStep("preview");
|
||||
} catch (err) {
|
||||
message.error("算子发布失败," + err.data.message);
|
||||
}
|
||||
};
|
||||
|
||||
const onFetchOperator = async (operatorId: string) => {
|
||||
// 编辑模式,加载已有算子信息逻辑待实现
|
||||
const { data } = await queryOperatorByIdUsingGet(operatorId);
|
||||
const configs = data.settings && typeof data.settings === "string"
|
||||
? JSON.parse(data.settings)
|
||||
: {};
|
||||
const defaultParams: Record<string, string> = {};
|
||||
Object.keys(configs).forEach((key) => {
|
||||
const { value } = configs[key];
|
||||
defaultParams[key] = value;
|
||||
});
|
||||
setParsedInfo({ ...data, configs, defaultParams});
|
||||
setUploadStep("configure");
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (id) {
|
||||
// 编辑模式,加载已有算子信息逻辑待实现
|
||||
onFetchOperator(id);
|
||||
}
|
||||
}, [id]);
|
||||
|
||||
return (
|
||||
<div className="flex-overflow-auto bg-gray-50">
|
||||
{/* Header */}
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex items-center gap-4">
|
||||
<Button type="text" onClick={() => navigate("/data/operator-market")}>
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
</Button>
|
||||
<h1 className="text-xl font-bold text-gray-900">
|
||||
{id ? "更新算子" : "上传算子"}
|
||||
</h1>
|
||||
</div>
|
||||
<div className="w-1/2">
|
||||
<Steps
|
||||
size="small"
|
||||
items={[
|
||||
{
|
||||
title: "上传文件",
|
||||
icon: <Upload />,
|
||||
},
|
||||
{
|
||||
title: "解析文件",
|
||||
icon: <Settings />,
|
||||
},
|
||||
{
|
||||
title: "配置信息",
|
||||
icon: <TagIcon />,
|
||||
},
|
||||
{
|
||||
title: "发布完成",
|
||||
icon: <CheckCircle />,
|
||||
},
|
||||
]}
|
||||
current={
|
||||
uploadStep === "upload"
|
||||
? 0
|
||||
: uploadStep === "parsing"
|
||||
? 1
|
||||
: uploadStep === "configure"
|
||||
? 2
|
||||
: 3
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-overflow-auto p-6 mt-4 bg-white border-card">
|
||||
<div className="flex-overflow-auto">
|
||||
{uploadStep === "upload" && (
|
||||
<UploadStep onUpload={handleFileUpload} isUploading={isUploading} />
|
||||
)}
|
||||
{uploadStep === "parsing" && (
|
||||
<ParsingStep
|
||||
parseProgress={taskList[0]?.percent || parsedInfo.percent || 0}
|
||||
uploadedFiles={taskList}
|
||||
/>
|
||||
)}
|
||||
{uploadStep === "configure" && (
|
||||
<ConfigureStep
|
||||
setParsedInfo={setParsedInfo}
|
||||
parseError={parseError}
|
||||
parsedInfo={parsedInfo}
|
||||
/>
|
||||
)}
|
||||
{uploadStep === "preview" && (
|
||||
<PreviewStep setUploadStep={setUploadStep} />
|
||||
)}
|
||||
</div>
|
||||
{uploadStep === "configure" && (
|
||||
<div className="flex justify-end gap-3 mt-8">
|
||||
<Button onClick={() => setUploadStep("upload")}>重新上传</Button>
|
||||
<Button type="primary" onClick={handlePublish}>
|
||||
{id ? "更新" : "发布"}算子
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,115 +1,115 @@
|
||||
import { Alert, Input, Form } from "antd";
|
||||
import TextArea from "antd/es/input/TextArea";
|
||||
import React, { useEffect } from "react";
|
||||
import ParamConfig from "@/pages/DataCleansing/Create/components/ParamConfig.tsx";
|
||||
|
||||
export default function ConfigureStep({
|
||||
parsedInfo,
|
||||
parseError,
|
||||
setParsedInfo,
|
||||
}) {
|
||||
const [form] = Form.useForm();
|
||||
|
||||
useEffect(() => {
|
||||
form.setFieldsValue(parsedInfo);
|
||||
}, [parsedInfo]);
|
||||
|
||||
const handleConfigChange = (
|
||||
operatorId: string,
|
||||
paramKey: string,
|
||||
value: any
|
||||
) => {
|
||||
setParsedInfo((op) =>
|
||||
op.id === operatorId
|
||||
? {
|
||||
...op,
|
||||
overrides: {
|
||||
...(op?.overrides || op?.defaultParams),
|
||||
[paramKey]: value,
|
||||
},
|
||||
}
|
||||
: op
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* 解析结果 */}
|
||||
{parseError && (
|
||||
<div className="mb-4">
|
||||
<Alert
|
||||
message="解析过程中发现问题"
|
||||
description={parseError}
|
||||
type="error"
|
||||
showIcon
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!parseError && parsedInfo && (
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
initialValues={parsedInfo}
|
||||
onValuesChange={(_, allValues) => {
|
||||
setParsedInfo({ ...parsedInfo, ...allValues });
|
||||
}}
|
||||
>
|
||||
{/* 基本信息 */}
|
||||
<h3 className="text-lg font-semibold text-gray-900">基本信息</h3>
|
||||
<Form.Item label="ID" name="id" rules={[{ required: true }]}>
|
||||
<Input value={parsedInfo.id} readOnly />
|
||||
</Form.Item>
|
||||
<Form.Item label="名称" name="name" rules={[{ required: true }]}>
|
||||
<Input value={parsedInfo.name} />
|
||||
</Form.Item>
|
||||
<Form.Item label="版本" name="version" rules={[{ required: true }]}>
|
||||
<Input value={parsedInfo.version} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="描述"
|
||||
name="description"
|
||||
rules={[{ required: false }]}
|
||||
>
|
||||
<TextArea value={parsedInfo.description} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="输入类型"
|
||||
name="inputs"
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Input value={parsedInfo.inputs} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="输出类型"
|
||||
name="outputs"
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Input value={parsedInfo.outputs} />
|
||||
</Form.Item>
|
||||
|
||||
{parsedInfo.configs && Object.keys(parsedInfo.configs).length > 0 && (
|
||||
<>
|
||||
<h3 className="text-lg font-semibold text-gray-900 mt-10 mb-2">
|
||||
高级配置
|
||||
</h3>
|
||||
<Form layout="vertical">
|
||||
{Object.entries(parsedInfo?.configs).map(([key, param]) => (
|
||||
<ParamConfig
|
||||
key={key}
|
||||
operator={parsedInfo}
|
||||
paramKey={key}
|
||||
param={param}
|
||||
onParamChange={handleConfigChange}
|
||||
/>
|
||||
))}
|
||||
</Form>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* <h3 className="text-lg font-semibold text-gray-900 mt-8">高级配置</h3> */}
|
||||
</Form>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
import { Alert, Input, Form } from "antd";
|
||||
import TextArea from "antd/es/input/TextArea";
|
||||
import React, { useEffect } from "react";
|
||||
import ParamConfig from "@/pages/DataCleansing/Create/components/ParamConfig.tsx";
|
||||
|
||||
export default function ConfigureStep({
|
||||
parsedInfo,
|
||||
parseError,
|
||||
setParsedInfo,
|
||||
}) {
|
||||
const [form] = Form.useForm();
|
||||
|
||||
useEffect(() => {
|
||||
form.setFieldsValue(parsedInfo);
|
||||
}, [parsedInfo]);
|
||||
|
||||
const handleConfigChange = (
|
||||
operatorId: string,
|
||||
paramKey: string,
|
||||
value: any
|
||||
) => {
|
||||
setParsedInfo((op) =>
|
||||
op.id === operatorId
|
||||
? {
|
||||
...op,
|
||||
overrides: {
|
||||
...(op?.overrides || op?.defaultParams),
|
||||
[paramKey]: value,
|
||||
},
|
||||
}
|
||||
: op
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* 解析结果 */}
|
||||
{parseError && (
|
||||
<div className="mb-4">
|
||||
<Alert
|
||||
message="解析过程中发现问题"
|
||||
description={parseError}
|
||||
type="error"
|
||||
showIcon
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!parseError && parsedInfo && (
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
initialValues={parsedInfo}
|
||||
onValuesChange={(_, allValues) => {
|
||||
setParsedInfo({ ...parsedInfo, ...allValues });
|
||||
}}
|
||||
>
|
||||
{/* 基本信息 */}
|
||||
<h3 className="text-lg font-semibold text-gray-900">基本信息</h3>
|
||||
<Form.Item label="ID" name="id" rules={[{ required: true }]}>
|
||||
<Input value={parsedInfo.id} readOnly />
|
||||
</Form.Item>
|
||||
<Form.Item label="名称" name="name" rules={[{ required: true }]}>
|
||||
<Input value={parsedInfo.name} />
|
||||
</Form.Item>
|
||||
<Form.Item label="版本" name="version" rules={[{ required: true }]}>
|
||||
<Input value={parsedInfo.version} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="描述"
|
||||
name="description"
|
||||
rules={[{ required: false }]}
|
||||
>
|
||||
<TextArea value={parsedInfo.description} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="输入类型"
|
||||
name="inputs"
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Input value={parsedInfo.inputs} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="输出类型"
|
||||
name="outputs"
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Input value={parsedInfo.outputs} />
|
||||
</Form.Item>
|
||||
|
||||
{parsedInfo.configs && Object.keys(parsedInfo.configs).length > 0 && (
|
||||
<>
|
||||
<h3 className="text-lg font-semibold text-gray-900 mt-10 mb-2">
|
||||
高级配置
|
||||
</h3>
|
||||
<Form layout="vertical">
|
||||
{Object.entries(parsedInfo?.configs).map(([key, param]) => (
|
||||
<ParamConfig
|
||||
key={key}
|
||||
operator={parsedInfo}
|
||||
paramKey={key}
|
||||
param={param}
|
||||
onParamChange={handleConfigChange}
|
||||
/>
|
||||
))}
|
||||
</Form>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* <h3 className="text-lg font-semibold text-gray-900 mt-8">高级配置</h3> */}
|
||||
</Form>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,50 +1,50 @@
|
||||
import { Progress } from "antd";
|
||||
import { Settings, FileText, CheckCircle } from "lucide-react";
|
||||
|
||||
export default function ParsingStep({ parseProgress, uploadedFiles }) {
|
||||
return (
|
||||
<div className="text-center py-2">
|
||||
<div className="w-24 h-24 mx-auto mb-6 bg-blue-50 rounded-full flex items-center justify-center">
|
||||
<Settings className="w-12 h-12 text-blue-500 animate-spin" />
|
||||
</div>
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-4">
|
||||
正在解析算子文件
|
||||
</h2>
|
||||
<p className="text-gray-600 mb-8">
|
||||
系统正在自动分析您的算子文件,提取配置信息...
|
||||
</p>
|
||||
|
||||
{/* 已上传文件列表 */}
|
||||
<div className="mb-8">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">已上传文件</h3>
|
||||
<div className="space-y-2">
|
||||
{uploadedFiles.map((file, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-center justify-between p-3 bg-gray-50 rounded-lg"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<FileText className="w-5 h-5 text-gray-500" />
|
||||
<span className="font-medium">{file.name}</span>
|
||||
<span className="text-sm text-gray-500">
|
||||
({(file.size / 1024).toFixed(1)} KB)
|
||||
</span>
|
||||
</div>
|
||||
<CheckCircle className="w-5 h-5 text-green-500" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 解析进度 */}
|
||||
<div className="max-w-md mx-auto">
|
||||
<Progress
|
||||
percent={parseProgress}
|
||||
status="active"
|
||||
strokeColor="#3B82F6"
|
||||
/>
|
||||
<p className="mt-2 text-sm text-gray-600">解析进度: {parseProgress}%</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
import { Progress } from "antd";
|
||||
import { Settings, FileText, CheckCircle } from "lucide-react";
|
||||
|
||||
export default function ParsingStep({ parseProgress, uploadedFiles }) {
|
||||
return (
|
||||
<div className="text-center py-2">
|
||||
<div className="w-24 h-24 mx-auto mb-6 bg-blue-50 rounded-full flex items-center justify-center">
|
||||
<Settings className="w-12 h-12 text-blue-500 animate-spin" />
|
||||
</div>
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-4">
|
||||
正在解析算子文件
|
||||
</h2>
|
||||
<p className="text-gray-600 mb-8">
|
||||
系统正在自动分析您的算子文件,提取配置信息...
|
||||
</p>
|
||||
|
||||
{/* 已上传文件列表 */}
|
||||
<div className="mb-8">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">已上传文件</h3>
|
||||
<div className="space-y-2">
|
||||
{uploadedFiles.map((file, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-center justify-between p-3 bg-gray-50 rounded-lg"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<FileText className="w-5 h-5 text-gray-500" />
|
||||
<span className="font-medium">{file.name}</span>
|
||||
<span className="text-sm text-gray-500">
|
||||
({(file.size / 1024).toFixed(1)} KB)
|
||||
</span>
|
||||
</div>
|
||||
<CheckCircle className="w-5 h-5 text-green-500" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 解析进度 */}
|
||||
<div className="max-w-md mx-auto">
|
||||
<Progress
|
||||
percent={parseProgress}
|
||||
status="active"
|
||||
strokeColor="#3B82F6"
|
||||
/>
|
||||
<p className="mt-2 text-sm text-gray-600">解析进度: {parseProgress}%</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
import { Button } from "antd";
|
||||
import { CheckCircle, Plus } from "lucide-react";
|
||||
import { useNavigate } from "react-router";
|
||||
|
||||
export default function PreviewStep({ setUploadStep }) {
|
||||
const navigate = useNavigate();
|
||||
return (
|
||||
<div className="text-center py-2">
|
||||
<div className="w-24 h-24 mx-auto mb-6 bg-green-50 rounded-full flex items-center justify-center">
|
||||
<CheckCircle className="w-12 h-12 text-green-500" />
|
||||
</div>
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-4">发布成功!</h2>
|
||||
<p className="text-gray-600 mb-8">您的算子已成功发布到算子市场</p>
|
||||
|
||||
<div className="flex justify-center gap-4">
|
||||
<Button onClick={() => setUploadStep("upload")}>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
继续上传
|
||||
</Button>
|
||||
<Button type="primary" onClick={() => navigate("/data/operator-market")}>
|
||||
返回主页
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
import { Button } from "antd";
|
||||
import { CheckCircle, Plus } from "lucide-react";
|
||||
import { useNavigate } from "react-router";
|
||||
|
||||
export default function PreviewStep({ setUploadStep }) {
|
||||
const navigate = useNavigate();
|
||||
return (
|
||||
<div className="text-center py-2">
|
||||
<div className="w-24 h-24 mx-auto mb-6 bg-green-50 rounded-full flex items-center justify-center">
|
||||
<CheckCircle className="w-12 h-12 text-green-500" />
|
||||
</div>
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-4">发布成功!</h2>
|
||||
<p className="text-gray-600 mb-8">您的算子已成功发布到算子市场</p>
|
||||
|
||||
<div className="flex justify-center gap-4">
|
||||
<Button onClick={() => setUploadStep("upload")}>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
继续上传
|
||||
</Button>
|
||||
<Button type="primary" onClick={() => navigate("/data/operator-market")}>
|
||||
返回主页
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,79 +1,79 @@
|
||||
import { Spin } from "antd";
|
||||
import { Upload, FileText } from "lucide-react";
|
||||
|
||||
export default function UploadStep({ isUploading, onUpload }) {
|
||||
const supportedFormats = [
|
||||
{ ext: ".zip", desc: "压缩包文件" },
|
||||
{ ext: ".tar", desc: "压缩包文件" },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="py-2 w-full text-center">
|
||||
<div className="w-24 h-24 mx-auto mb-6 bg-blue-50 rounded-full flex items-center justify-center">
|
||||
<Upload className="w-12 h-12 text-blue-500" />
|
||||
</div>
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-4">上传算子文件</h2>
|
||||
<p className="text-gray-600 mb-8">
|
||||
支持多种格式的算子文件,系统将自动解析配置信息
|
||||
</p>
|
||||
|
||||
{/* 支持的格式 */}
|
||||
<div className="mb-8">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">
|
||||
支持的文件格式
|
||||
</h3>
|
||||
<div className="flex gap-4">
|
||||
{supportedFormats.map((format, index) => (
|
||||
<div key={index} className="p-3 border border-gray-200 rounded-lg flex-1">
|
||||
<div className="font-medium text-gray-900">{format.ext}</div>
|
||||
<div className="text-sm text-gray-500">{format.desc}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 文件上传区域 */}
|
||||
<div
|
||||
className="border-2 border-dashed border-gray-300 rounded-lg p-8 hover:border-blue-400 transition-colors cursor-pointer"
|
||||
onDrop={(e) => {
|
||||
e.preventDefault();
|
||||
const files = e.dataTransfer.files;
|
||||
if (files.length > 0) {
|
||||
onUpload(files);
|
||||
}
|
||||
}}
|
||||
onDragOver={(e) => e.preventDefault()}
|
||||
onClick={() => {
|
||||
const input = document.createElement("input");
|
||||
input.type = "file";
|
||||
input.multiple = false;
|
||||
input.accept = supportedFormats.map((f) => f.ext).join(",");
|
||||
input.onchange = (e) => {
|
||||
const files = (e.target as HTMLInputElement).files;
|
||||
if (files) {
|
||||
onUpload(files);
|
||||
}
|
||||
};
|
||||
input.click();
|
||||
}}
|
||||
>
|
||||
{isUploading ? (
|
||||
<div className="flex flex-col items-center">
|
||||
<Spin size="large" />
|
||||
<p className="mt-4 text-gray-600">正在上传文件...</p>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<FileText className="w-12 h-12 text-gray-400 mx-auto mb-4" />
|
||||
<p className="text-lg text-gray-600 mb-2">
|
||||
拖拽文件到此处或点击选择文件
|
||||
</p>
|
||||
<p className="text-sm text-gray-500">
|
||||
仅支持单个文件上传
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
import { Spin } from "antd";
|
||||
import { Upload, FileText } from "lucide-react";
|
||||
|
||||
export default function UploadStep({ isUploading, onUpload }) {
|
||||
const supportedFormats = [
|
||||
{ ext: ".zip", desc: "压缩包文件" },
|
||||
{ ext: ".tar", desc: "压缩包文件" },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="py-2 w-full text-center">
|
||||
<div className="w-24 h-24 mx-auto mb-6 bg-blue-50 rounded-full flex items-center justify-center">
|
||||
<Upload className="w-12 h-12 text-blue-500" />
|
||||
</div>
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-4">上传算子文件</h2>
|
||||
<p className="text-gray-600 mb-8">
|
||||
支持多种格式的算子文件,系统将自动解析配置信息
|
||||
</p>
|
||||
|
||||
{/* 支持的格式 */}
|
||||
<div className="mb-8">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">
|
||||
支持的文件格式
|
||||
</h3>
|
||||
<div className="flex gap-4">
|
||||
{supportedFormats.map((format, index) => (
|
||||
<div key={index} className="p-3 border border-gray-200 rounded-lg flex-1">
|
||||
<div className="font-medium text-gray-900">{format.ext}</div>
|
||||
<div className="text-sm text-gray-500">{format.desc}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 文件上传区域 */}
|
||||
<div
|
||||
className="border-2 border-dashed border-gray-300 rounded-lg p-8 hover:border-blue-400 transition-colors cursor-pointer"
|
||||
onDrop={(e) => {
|
||||
e.preventDefault();
|
||||
const files = e.dataTransfer.files;
|
||||
if (files.length > 0) {
|
||||
onUpload(files);
|
||||
}
|
||||
}}
|
||||
onDragOver={(e) => e.preventDefault()}
|
||||
onClick={() => {
|
||||
const input = document.createElement("input");
|
||||
input.type = "file";
|
||||
input.multiple = false;
|
||||
input.accept = supportedFormats.map((f) => f.ext).join(",");
|
||||
input.onchange = (e) => {
|
||||
const files = (e.target as HTMLInputElement).files;
|
||||
if (files) {
|
||||
onUpload(files);
|
||||
}
|
||||
};
|
||||
input.click();
|
||||
}}
|
||||
>
|
||||
{isUploading ? (
|
||||
<div className="flex flex-col items-center">
|
||||
<Spin size="large" />
|
||||
<p className="mt-4 text-gray-600">正在上传文件...</p>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<FileText className="w-12 h-12 text-gray-400 mx-auto mb-4" />
|
||||
<p className="text-lg text-gray-600 mb-2">
|
||||
拖拽文件到此处或点击选择文件
|
||||
</p>
|
||||
<p className="text-sm text-gray-500">
|
||||
仅支持单个文件上传
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,149 +1,149 @@
|
||||
import React, { useEffect } from "react";
|
||||
|
||||
import { useState } from "react";
|
||||
import {Card, Breadcrumb, message} from "antd";
|
||||
import {
|
||||
DeleteOutlined, StarFilled,
|
||||
StarOutlined, UploadOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import {Clock, GitBranch} from "lucide-react";
|
||||
import DetailHeader from "@/components/DetailHeader";
|
||||
import {Link, useNavigate, useParams} from "react-router";
|
||||
import Overview from "./components/Overview";
|
||||
import Install from "./components/Install";
|
||||
|
||||
import {deleteOperatorByIdUsingDelete, queryOperatorByIdUsingGet, updateOperatorByIdUsingPut} from "../operator.api";
|
||||
import { OperatorI } from "../operator.model";
|
||||
import { mapOperator } from "../operator.const";
|
||||
|
||||
export default function OperatorPluginDetail() {
|
||||
const { id } = useParams(); // 获取动态路由参数
|
||||
const navigate = useNavigate();
|
||||
const [activeTab, setActiveTab] = useState("overview");
|
||||
const [isStar, setIsStar] = useState(false);
|
||||
const [operator, setOperator] = useState<OperatorI | null>(null);
|
||||
|
||||
const fetchOperator = async () => {
|
||||
try {
|
||||
const { data } = await queryOperatorByIdUsingGet(id as unknown as number);
|
||||
setOperator(mapOperator(data));
|
||||
setIsStar(data.isStar)
|
||||
} catch (error) {
|
||||
setOperator("error");
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchOperator();
|
||||
}, [id]);
|
||||
|
||||
if (!operator) {
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
|
||||
if (operator === "error") {
|
||||
return (
|
||||
<div className="text-red-500">
|
||||
Failed to load operator details. Please try again later.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const handleStar = async () => {
|
||||
const data = {
|
||||
id: operator.id,
|
||||
isStar: !isStar
|
||||
};
|
||||
await updateOperatorByIdUsingPut(operator.id, data)
|
||||
setIsStar(!isStar)
|
||||
}
|
||||
|
||||
const handleDelete = async () => {
|
||||
await deleteOperatorByIdUsingDelete(operator.id);
|
||||
navigate("/data/operator-market");
|
||||
message.success("算子删除成功");
|
||||
};
|
||||
|
||||
// 模拟算子数据
|
||||
const statistics = [
|
||||
{
|
||||
icon: <GitBranch className="text-blue-400 w-4 h-4" />,
|
||||
label: "",
|
||||
value: "v" + operator?.version,
|
||||
},
|
||||
{
|
||||
icon: <Clock className="text-blue-400 w-4 h-4" />,
|
||||
label: "",
|
||||
value: operator?.updatedAt,
|
||||
},
|
||||
];
|
||||
|
||||
const operations = [
|
||||
{
|
||||
key: "favorite",
|
||||
label: "收藏",
|
||||
icon: (isStar ? (
|
||||
<StarFilled style={{ color: '#f59e0b' }} />
|
||||
) : (
|
||||
<StarOutlined />
|
||||
)
|
||||
),
|
||||
onClick: handleStar,
|
||||
},
|
||||
{
|
||||
key: "update",
|
||||
label: "更新",
|
||||
icon: <UploadOutlined />,
|
||||
onClick: () => navigate("/data/operator-market/create/" + operator.id),
|
||||
},
|
||||
{
|
||||
key: "delete",
|
||||
label: "删除",
|
||||
danger: true,
|
||||
confirm: {
|
||||
title: "确认删除当前算子?",
|
||||
description: "删除后该算子将无法恢复,请谨慎操作。",
|
||||
okText: "删除",
|
||||
cancelText: "取消",
|
||||
okType: "danger"
|
||||
},
|
||||
icon: <DeleteOutlined />,
|
||||
onClick: handleDelete,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col gap-4">
|
||||
{/* Header */}
|
||||
<Breadcrumb
|
||||
items={[
|
||||
{
|
||||
title: <Link to="/data/operator-market">算子市场</Link>,
|
||||
href: "/data/operator-market",
|
||||
},
|
||||
{
|
||||
title: operator?.name,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<DetailHeader
|
||||
data={operator}
|
||||
statistics={statistics}
|
||||
operations={operations}
|
||||
/>
|
||||
<Card
|
||||
tabList={[
|
||||
{
|
||||
key: "overview",
|
||||
label: "概览",
|
||||
},
|
||||
]}
|
||||
activeTabKey={activeTab}
|
||||
onTabChange={setActiveTab}
|
||||
>
|
||||
{activeTab === "overview" && <Overview operator={operator} />}
|
||||
{activeTab === "service" && <Install operator={operator} />}
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
import React, { useEffect } from "react";
|
||||
|
||||
import { useState } from "react";
|
||||
import {Card, Breadcrumb, message} from "antd";
|
||||
import {
|
||||
DeleteOutlined, StarFilled,
|
||||
StarOutlined, UploadOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import {Clock, GitBranch} from "lucide-react";
|
||||
import DetailHeader from "@/components/DetailHeader";
|
||||
import {Link, useNavigate, useParams} from "react-router";
|
||||
import Overview from "./components/Overview";
|
||||
import Install from "./components/Install";
|
||||
|
||||
import {deleteOperatorByIdUsingDelete, queryOperatorByIdUsingGet, updateOperatorByIdUsingPut} from "../operator.api";
|
||||
import { OperatorI } from "../operator.model";
|
||||
import { mapOperator } from "../operator.const";
|
||||
|
||||
export default function OperatorPluginDetail() {
|
||||
const { id } = useParams(); // 获取动态路由参数
|
||||
const navigate = useNavigate();
|
||||
const [activeTab, setActiveTab] = useState("overview");
|
||||
const [isStar, setIsStar] = useState(false);
|
||||
const [operator, setOperator] = useState<OperatorI | null>(null);
|
||||
|
||||
const fetchOperator = async () => {
|
||||
try {
|
||||
const { data } = await queryOperatorByIdUsingGet(id as unknown as number);
|
||||
setOperator(mapOperator(data));
|
||||
setIsStar(data.isStar)
|
||||
} catch (error) {
|
||||
setOperator("error");
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchOperator();
|
||||
}, [id]);
|
||||
|
||||
if (!operator) {
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
|
||||
if (operator === "error") {
|
||||
return (
|
||||
<div className="text-red-500">
|
||||
Failed to load operator details. Please try again later.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const handleStar = async () => {
|
||||
const data = {
|
||||
id: operator.id,
|
||||
isStar: !isStar
|
||||
};
|
||||
await updateOperatorByIdUsingPut(operator.id, data)
|
||||
setIsStar(!isStar)
|
||||
}
|
||||
|
||||
const handleDelete = async () => {
|
||||
await deleteOperatorByIdUsingDelete(operator.id);
|
||||
navigate("/data/operator-market");
|
||||
message.success("算子删除成功");
|
||||
};
|
||||
|
||||
// 模拟算子数据
|
||||
const statistics = [
|
||||
{
|
||||
icon: <GitBranch className="text-blue-400 w-4 h-4" />,
|
||||
label: "",
|
||||
value: "v" + operator?.version,
|
||||
},
|
||||
{
|
||||
icon: <Clock className="text-blue-400 w-4 h-4" />,
|
||||
label: "",
|
||||
value: operator?.updatedAt,
|
||||
},
|
||||
];
|
||||
|
||||
const operations = [
|
||||
{
|
||||
key: "favorite",
|
||||
label: "收藏",
|
||||
icon: (isStar ? (
|
||||
<StarFilled style={{ color: '#f59e0b' }} />
|
||||
) : (
|
||||
<StarOutlined />
|
||||
)
|
||||
),
|
||||
onClick: handleStar,
|
||||
},
|
||||
{
|
||||
key: "update",
|
||||
label: "更新",
|
||||
icon: <UploadOutlined />,
|
||||
onClick: () => navigate("/data/operator-market/create/" + operator.id),
|
||||
},
|
||||
{
|
||||
key: "delete",
|
||||
label: "删除",
|
||||
danger: true,
|
||||
confirm: {
|
||||
title: "确认删除当前算子?",
|
||||
description: "删除后该算子将无法恢复,请谨慎操作。",
|
||||
okText: "删除",
|
||||
cancelText: "取消",
|
||||
okType: "danger"
|
||||
},
|
||||
icon: <DeleteOutlined />,
|
||||
onClick: handleDelete,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col gap-4">
|
||||
{/* Header */}
|
||||
<Breadcrumb
|
||||
items={[
|
||||
{
|
||||
title: <Link to="/data/operator-market">算子市场</Link>,
|
||||
href: "/data/operator-market",
|
||||
},
|
||||
{
|
||||
title: operator?.name,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<DetailHeader
|
||||
data={operator}
|
||||
statistics={statistics}
|
||||
operations={operations}
|
||||
/>
|
||||
<Card
|
||||
tabList={[
|
||||
{
|
||||
key: "overview",
|
||||
label: "概览",
|
||||
},
|
||||
]}
|
||||
activeTabKey={activeTab}
|
||||
onTabChange={setActiveTab}
|
||||
>
|
||||
{activeTab === "overview" && <Overview operator={operator} />}
|
||||
{activeTab === "service" && <Install operator={operator} />}
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,34 +1,34 @@
|
||||
import { Card } from "antd";
|
||||
import { Badge, ChevronRight } from "lucide-react";
|
||||
|
||||
export default function ChangeLog({ operator }) {
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
{operator.changelog.map((version, index) => (
|
||||
<Card key={index}>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-gray-900">
|
||||
版本 {version.version}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-600">{version.date}</p>
|
||||
</div>
|
||||
{index === 0 && (
|
||||
<Badge className="bg-blue-100 text-blue-800 border border-blue-200">
|
||||
最新版本
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<ul className="space-y-2">
|
||||
{version.changes.map((change, changeIndex) => (
|
||||
<li key={changeIndex} className="flex items-start gap-2">
|
||||
<ChevronRight className="w-4 h-4 text-gray-400 mt-0.5 flex-shrink-0" />
|
||||
<span className="text-gray-700">{change}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
import { Card } from "antd";
|
||||
import { Badge, ChevronRight } from "lucide-react";
|
||||
|
||||
export default function ChangeLog({ operator }) {
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
{operator.changelog.map((version, index) => (
|
||||
<Card key={index}>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-gray-900">
|
||||
版本 {version.version}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-600">{version.date}</p>
|
||||
</div>
|
||||
{index === 0 && (
|
||||
<Badge className="bg-blue-100 text-blue-800 border border-blue-200">
|
||||
最新版本
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<ul className="space-y-2">
|
||||
{version.changes.map((change, changeIndex) => (
|
||||
<li key={changeIndex} className="flex items-start gap-2">
|
||||
<ChevronRight className="w-4 h-4 text-gray-400 mt-0.5 flex-shrink-0" />
|
||||
<span className="text-gray-700">{change}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user