init datamate

This commit is contained in:
Dallas98
2025-10-21 23:00:48 +08:00
commit 1c97afed7d
692 changed files with 135442 additions and 0 deletions

View File

@@ -0,0 +1,129 @@
import { Button, Input, Popover, theme, Tag } from "antd";
import { PlusOutlined } from "@ant-design/icons";
import { useEffect, useMemo, useState } from "react";
interface Tag {
id: number;
name: string;
color: string;
}
interface AddTagPopoverProps {
tags: Tag[];
onFetchTags?: () => Promise<Tag[]>;
onAddTag?: (tag: Tag) => void;
onCreateAndTag?: (tagName: string) => void;
}
export default function AddTagPopover({
tags,
onFetchTags,
onAddTag,
onCreateAndTag,
}: AddTagPopoverProps) {
const { token } = theme.useToken();
const [showPopover, setShowPopover] = useState(false);
const [newTag, setNewTag] = useState("");
const [allTags, setAllTags] = useState<Tag[]>([]);
const tagsSet = useMemo(() => new Set(tags.map((tag) => tag.id)), [tags]);
const fetchTags = async () => {
if (onFetchTags && showPopover) {
const data = await onFetchTags?.();
setAllTags(data || []);
}
};
useEffect(() => {
fetchTags();
}, [showPopover]);
const availableTags = useMemo(() => {
return allTags.filter((tag) => !tagsSet.has(tag.id));
}, [allTags, tagsSet]);
const handleCreateAndAddTag = () => {
if (newTag.trim()) {
onCreateAndTag?.(newTag.trim());
setNewTag("");
}
setShowPopover(false);
};
const tagPlusStyle: React.CSSProperties = {
height: 22,
background: token.colorBgContainer,
borderStyle: "dashed",
};
return (
<>
<Popover
open={showPopover}
trigger="click"
placement="bottom"
content={
<div className="space-y-4 w-[300px]">
<h4 className="font-medium border-b pb-2 border-gray-100">
</h4>
{/* Available Tags */}
<div className="space-y-2">
<h5 className="text-sm"></h5>
<div className="max-h-32 overflow-y-auto space-y-1">
{availableTags.map((tag) => (
<span
key={tag.id}
className="h-7 w-full justify-start text-xs cursor-pointer flex items-center px-2 rounded hover:bg-gray-100"
onClick={() => {
onAddTag?.(tag.name);
setShowPopover(false);
}}
>
<PlusOutlined className="w-3 h-3 mr-1" />
{tag.name}
</span>
))}
</div>
</div>
{/* Create New Tag */}
<div className="space-y-2 border-t border-gray-100 pt-3">
<h5 className="text-sm"></h5>
<div className="flex gap-2">
<Input
placeholder="输入新标签名称..."
value={newTag}
onChange={(e) => setNewTag(e.target.value)}
className="h-8 text-sm"
/>
<Button
onClick={() => handleCreateAndAddTag()}
disabled={!newTag.trim()}
type="primary"
>
</Button>
</div>
</div>
<Button block onClick={() => setShowPopover(false)}>
</Button>
</div>
}
>
<Tag
style={tagPlusStyle}
icon={<PlusOutlined />}
className="cursor-pointer"
onClick={() => setShowPopover(true)}
>
</Tag>
</Popover>
</>
);
}

View File

@@ -0,0 +1,291 @@
import React, { useState, useEffect, useRef } from "react";
import { Tag, Pagination, Dropdown, Tooltip, Empty, Popover } from "antd";
import {
EllipsisOutlined,
ClockCircleOutlined,
StarFilled,
} from "@ant-design/icons";
import type { ItemType } from "antd/es/menu/interface";
import { formatDateTime } from "@/utils/unit";
interface BaseCardDataType {
id: string | number;
name: string;
type: string;
icon?: React.JSX.Element;
iconColor?: string;
status: {
label: string;
icon?: React.JSX.Element;
color?: string;
} | null;
description: string;
tags?: string[];
statistics?: { label: string; value: string | number }[];
updatedAt?: string;
}
interface CardViewProps<T> {
data: T[];
pagination: {
[key: string]: any;
current: number;
pageSize: number;
total: number;
};
operations:
| {
key: string;
label: string;
icon?: React.JSX.Element;
onClick?: (item: T) => void;
}[]
| ((item: T) => ItemType[]);
onView?: (item: T) => void;
onFavorite?: (item: T) => void;
isFavorite?: (item: T) => boolean;
}
// 标签渲染组件
const TagsRenderer = ({ tags }: { tags?: any[] }) => {
const [visibleTags, setVisibleTags] = useState<any[]>([]);
const [hiddenTags, setHiddenTags] = useState<any[]>([]);
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!tags || tags.length === 0) return;
const calculateVisibleTags = () => {
if (!containerRef.current) return;
const containerWidth = containerRef.current.offsetWidth;
const tempDiv = document.createElement("div");
tempDiv.style.visibility = "hidden";
tempDiv.style.position = "absolute";
tempDiv.style.top = "-9999px";
tempDiv.className = "flex flex-wrap gap-1";
document.body.appendChild(tempDiv);
let totalWidth = 0;
let visibleCount = 0;
const tagElements: HTMLElement[] = [];
// 为每个tag创建临时元素来测量宽度
tags.forEach((tag, index) => {
const tagElement = document.createElement("span");
tagElement.className = "ant-tag ant-tag-default";
tagElement.style.margin = "2px";
tagElement.textContent = typeof tag === "string" ? tag : tag.name;
tempDiv.appendChild(tagElement);
tagElements.push(tagElement);
const tagWidth = tagElement.offsetWidth + 4; // 加上gap的宽度
// 如果不是最后一个标签,需要预留+n标签的空间
const plusTagWidth = index < tags.length - 1 ? 35 : 0; // +n标签大约35px宽度
if (totalWidth + tagWidth + plusTagWidth <= containerWidth) {
totalWidth += tagWidth;
visibleCount++;
} else {
// 如果当前标签放不下,且已经有可见标签,则停止
if (visibleCount > 0) return;
// 如果是第一个标签就放不下,至少显示一个
if (index === 0) {
totalWidth += tagWidth;
visibleCount = 1;
}
}
});
document.body.removeChild(tempDiv);
setVisibleTags(tags.slice(0, visibleCount));
setHiddenTags(tags.slice(visibleCount));
};
// 延迟执行以确保DOM已渲染
const timer = setTimeout(calculateVisibleTags, 0);
// 监听窗口大小变化
const handleResize = () => {
calculateVisibleTags();
};
window.addEventListener("resize", handleResize);
return () => {
clearTimeout(timer);
window.removeEventListener("resize", handleResize);
};
}, [tags]);
if (!tags || tags.length === 0) return null;
const popoverContent = (
<div className="max-w-xs">
<div className="flex flex-wrap gap-1">
{hiddenTags.map((tag, index) => (
<Tag key={index}>{typeof tag === "string" ? tag : tag.name}</Tag>
))}
</div>
</div>
);
return (
<div ref={containerRef} className="flex flex-wrap gap-1 w-full">
{visibleTags.map((tag, index) => (
<Tag key={index}>{typeof tag === "string" ? tag : tag.name}</Tag>
))}
{hiddenTags.length > 0 && (
<Popover
content={popoverContent}
title="更多标签"
trigger="hover"
placement="topLeft"
>
<Tag className="cursor-pointer bg-gray-100 border-gray-300 text-gray-600 hover:bg-gray-200">
+{hiddenTags.length}
</Tag>
</Popover>
)}
</div>
);
};
function CardView<T extends BaseCardDataType>(props: CardViewProps<T>) {
const { data, pagination, operations, onView, onFavorite, isFavorite } =
props;
if (data.length === 0) {
return (
<div className="flex flex-col items-center justify-center h-full text-gray-500">
<Empty />
</div>
);
}
const ops = (item) =>
typeof operations === "function" ? operations(item) : operations;
return (
<div className="flex-1 flex flex-col overflow-auto">
<div className="overflow-auto grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4 gap-4">
{data.map((item) => (
<div
key={item.id}
className="border border-gray-100 rounded-lg p-4 bg-white hover:shadow-lg transition-shadow duration-200"
>
<div className="flex flex-col space-y-4 h-full">
{/* Header */}
<div className="flex items-start justify-between">
<div className="flex items-center gap-3 min-w-0">
{item?.icon && (
<div
className={`flex-shrink-0 w-12 h-12 ${
item?.iconColor ||
"bg-gradient-to-br from-blue-100 to-blue-200"
} rounded-lg flex items-center justify-center`}
>
{item?.icon}
</div>
)}
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<h3
className={`text-base flex-1 text-ellipsis overflow-hidden whitespace-nowrap font-semibold text-gray-900 truncate ${
onView ? "cursor-pointer hover:text-blue-600" : ""
}`}
onClick={() => onView?.(item)}
>
{item?.name}
</h3>
{item?.status && (
<Tag color={item?.status?.color}>
<div className="flex items-center gap-2 text-xs py-0.5">
<span>{item?.status?.icon}</span>
<span>{item?.status?.label}</span>
</div>
</Tag>
)}
</div>
</div>
</div>
{onFavorite && (
<StarFilled
style={{
fontSize: "16px",
color: isFavorite?.(item) ? "#ffcc00ff" : "#d1d5db",
cursor: "pointer",
}}
onClick={() => onFavorite?.(item)}
/>
)}
</div>
<div className="flex-1 flex flex-col justify-end">
{/* Tags */}
<TagsRenderer tags={item?.tags || []} />
{/* Description */}
<p className="text-gray-600 text-xs text-ellipsis overflow-hidden whitespace-nowrap text-xs line-clamp-2 mt-2">
<Tooltip title={item?.description}>
{item?.description}
</Tooltip>
</p>
{/* Statistics */}
<div className="grid grid-cols-2 gap-4 py-3">
{item?.statistics?.map((stat, idx) => (
<div key={idx}>
<div className="text-sm text-gray-500">
{stat?.label}:
</div>
<div className="text-base font-semibold text-gray-900">
{stat?.value}
</div>
</div>
))}
</div>
</div>
{/* Actions */}
<div className="flex items-center justify-between pt-3 border-t border-t-gray-200">
<div className=" text-gray-500 text-right">
<div className="flex items-center gap-1">
<ClockCircleOutlined className="w-4 h-4" />{" "}
{formatDateTime(item?.updatedAt)}
</div>
</div>
{operations && (
<Dropdown
trigger={["click"]}
menu={{
items: ops(item),
onClick: ({ key }) => {
const operation = ops(item).find(
(op) => op.key === key
);
if (operation?.onClick) {
operation.onClick(item);
}
},
}}
>
<div className="cursor-pointer">
<EllipsisOutlined style={{ fontSize: 24 }} />
</div>
</Dropdown>
)}
</div>
</div>
</div>
))}
</div>
<div className="flex justify-end mt-6">
<Pagination {...pagination} />
</div>
</div>
);
}
export default CardView;

View File

@@ -0,0 +1,137 @@
import React from "react";
import { Database } from "lucide-react";
import { Card, Dropdown, Button, Tag, Tooltip } from "antd";
import type { ItemType } from "antd/es/menu/interface";
import AddTagPopover from "./AddTagPopover";
interface StatisticItem {
icon: React.ReactNode;
label: string;
value: string | number;
}
interface OperationItem {
key: string;
label: string;
icon?: React.ReactNode;
isDropdown?: boolean;
items?: ItemType[];
onMenuClick?: (key: string) => void;
onClick?: () => void;
danger?: boolean;
}
interface TagConfig {
showAdd: boolean;
tags: { id: number; name: string; color: string }[];
onFetchTags?: () => Promise<{
data: { id: number; name: string; color: string }[];
}>;
onAddTag?: (tag: { id: number; name: string; color: string }) => void;
onCreateAndTag?: (tagName: string) => void;
}
interface DetailHeaderProps<T> {
data: T;
statistics: StatisticItem[];
operations: OperationItem[];
tagConfig?: TagConfig;
}
function DetailHeader<T>({
data,
statistics,
operations,
tagConfig,
}: DetailHeaderProps<T>): React.ReactNode {
return (
<Card>
<div className="flex items-start justify-between">
<div className="flex items-start gap-4 flex-1">
<div
className={`w-16 h-16 text-white rounded-xl flex items-center justify-center shadow-lg ${
data?.iconColor
? data.iconColor
: "bg-gradient-to-br from-blue-100 to-blue-200"
}`}
>
{data?.icon || <Database className="w-8 h-8" />}
</div>
<div className="flex-1">
<div className="flex items-center gap-3 mb-2">
<h1 className="text-lg font-bold text-gray-900">{data.name}</h1>
{data?.status && (
<Tag color={data.status?.color}>
<div className="flex items-center gap-2 text-xs">
<span>{data.status?.icon}</span>
<span>{data.status?.label}</span>
</div>
</Tag>
)}
</div>
{data?.tags && (
<div className="flex flex-wrap mb-2">
{data?.tags?.map((tag) => (
<Tag key={tag.id} className="mr-1">
{tag.name}
</Tag>
))}
{tagConfig?.showAdd && (
<AddTagPopover
tags={tagConfig.tags}
onFetchTags={tagConfig.onFetchTags}
onAddTag={tagConfig.onAddTag}
onCreateAndTag={tagConfig.onCreateAndTag}
/>
)}
</div>
)}
<p className="text-gray-700 mb-4">{data.description}</p>
<div className="flex items-center gap-6 text-sm">
{statistics.map((stat) => (
<div key={stat.key} className="flex items-center gap-1">
{stat.icon}
<span>{stat.value}</span>
</div>
))}
</div>
</div>
</div>
<div className="flex items-center gap-2">
{operations.map((op) => {
if (op.isDropdown) {
return (
<Dropdown
key={op.key}
menu={{
items: op.items as ItemType[],
onClick: op.onMenuClick,
}}
>
<Tooltip title={op.label}>
<Button icon={op.icon} />
</Tooltip>
</Dropdown>
);
}
return (
<Tooltip key={op.key} title={op.label}>
<Button
key={op.key}
onClick={op.onClick}
className={
op.danger
? "text-red-600 border-red-200 bg-transparent"
: ""
}
icon={op.icon}
/>
</Tooltip>
);
})}
</div>
</div>
</Card>
);
}
export default DetailHeader;

View File

@@ -0,0 +1,27 @@
import { Button } from "antd";
const DevelopmentInProgress = ({ showHome = true }) => {
return (
<div className="mt-40 flex flex-col items-center justify-center">
<div className="hero-icon">🚧</div>
<h1 className="text-2xl font-bold"></h1>
<p className="mt-4">
<b>2025.10.30</b>
</p>
{showHome && (
<Button
type="primary"
className="mt-6"
onClick={() => {
window.location.href = "/";
}}
>
</Button>
)}
</div>
);
};
export default DevelopmentInProgress;

View File

@@ -0,0 +1,191 @@
import React, { Component } from "react";
import { Button, Modal } from "antd";
interface ErrorContextType {
hasError: boolean;
error: Error | null;
errorInfo: { componentStack: string } | null;
}
const ErrorContext = React.createContext<ErrorContextType>({
hasError: false,
error: null,
errorInfo: null,
});
interface ErrorBoundaryState {
hasError: boolean;
error: Error | null;
errorInfo: { componentStack: string } | null;
errorTimestamp: string | null;
}
interface ErrorBoundaryProps {
children?: React.ReactNode;
onReset?: () => void;
showDetails?: boolean;
}
export default class ErrorBoundary extends Component<
ErrorBoundaryProps,
ErrorBoundaryState
> {
constructor(props: ErrorBoundaryProps) {
super(props);
this.state = {
hasError: false,
error: null,
errorInfo: null,
errorTimestamp: null,
};
}
static getDerivedStateFromError(error: any) {
// 更新 state 使下一次渲染能够显示降级 UI
return {
hasError: true,
error: error,
errorTimestamp: new Date().toISOString(),
};
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
// 错误统计
this.setState({
error,
errorInfo,
hasError: true,
});
// 在实际应用中,这里可以集成错误报告服务
this.logErrorToService(error, errorInfo);
// 开发环境下在控制台显示详细错误
if (process.env.NODE_ENV === "development") {
console.error("ErrorBoundary 捕获到错误:", error);
console.error("错误详情:", errorInfo);
}
}
logErrorToService = (error: Error, errorInfo: React.ErrorInfo) => {
// 这里可以集成 Sentry、LogRocket 等错误监控服务
const errorData = {
error: error.toString(),
errorInfo: errorInfo.componentStack,
timestamp: this.state.errorTimestamp,
url: window.location.href,
userAgent: navigator.userAgent,
};
// 模拟发送错误日志
console.log("发送错误日志到监控服务:", errorData);
// 实际使用时取消注释并配置您的错误监控服务
/*
if (window.Sentry) {
window.Sentry.captureException(error, { extra: errorInfo });
}
*/
};
handleReset = () => {
this.setState({
hasError: false,
error: null,
errorInfo: null,
errorTimestamp: null,
});
// 可选:重新加载页面或执行其他恢复操作
if (this.props.onReset) {
this.props.onReset();
}
};
handleReload = () => {
window.location.reload();
};
handleGoHome = () => {
window.location.href = "/";
};
renderErrorDetails = () => {
const { error, errorInfo } = this.state;
if (!this.props.showDetails) return null;
return (
<div className="bg-gray-100 p-4 mt-4 text-left rounded">
<div className="mt-2">
<strong>:</strong>
<pre className="bg-gray-600 px-4 py-2 rounded text-white overflow-auto">
{error?.toString()}
</pre>
</div>
{errorInfo && (
<div className="mt-2">
<strong>:</strong>
<pre className="bg-gray-600 max-h-100 px-4 py-2 rounded text-white overflow-auto">
{errorInfo.componentStack}
</pre>
</div>
)}
</div>
);
};
render() {
if (this.state.hasError) {
return (
<Modal visible width={1000} footer={null} closable={false}>
<div className="text-center p-6">
<div className="text-3xl"></div>
<h1 className="text-xl p-2"></h1>
<p className="text-sm text-gray-400"></p>
<div className="flex justify-center gap-4 my-4">
<Button onClick={this.handleReload}></Button>
<Button type="primary" onClick={this.handleGoHome}>
</Button>
</div>
{this.renderErrorDetails()}
<div className="mt-4 border-t border-gray-100 pt-4 text-center">
<p className="text-sm text-gray-500">
</p>
<small className="text-xs text-gray-400">
ID: {this.state.errorTimestamp}
</small>
</div>
</div>
</Modal>
);
}
return (
<ErrorContext.Provider
value={{
hasError: this.state.hasError,
error: this.state.error,
errorInfo: this.state.errorInfo,
}}
>
{this.props.children}
</ErrorContext.Provider>
);
}
}
export function withErrorBoundary(
Component: React.ComponentType
): React.ComponentType {
return (props) => (
<ErrorBoundary showDetails={process.env.NODE_ENV === "development"}>
<Component {...props} />
</ErrorBoundary>
);
}

View File

@@ -0,0 +1,70 @@
import React from "react";
import { Card } from "antd";
interface RadioCardOption {
value: string;
label: string;
description?: string;
icon?: SVGAElement | React.FC<React.SVGProps<SVGElement>>;
color?: string;
}
interface RadioCardProps {
options: RadioCardOption[];
value: string;
onChange: (value: string) => void;
className?: string;
}
const RadioCard: React.FC<RadioCardProps> = ({
options,
value,
onChange,
className,
}) => {
return (
<div
className={`grid gap-4 grid-cols-1 sm:grid-cols-2 md:grid-cols-3 ${
className || ""
}`}
style={{ gridTemplateColumns: "repeat(auto-fit, minmax(220px, 1fr))" }}
>
{options.map((option) => (
<div
key={option.value}
className="border border-gray-200 rounded-lg hover:shadow-lg p-4 text-center"
style={{
borderColor: value === option.value ? "#1677ff" : undefined,
background: value === option.value ? "#e6f7ff" : undefined,
cursor: "pointer",
}}
onClick={() => onChange(option.value)}
>
<option.icon
className={`w-8 h-8 mx-auto mb-2 ${
value === option.value ? "text-blue-500" : "text-gray-400"
}`}
/>
<h3
className={`font-medium text-sm mb-1 ${
value === option.value ? "text-blue-500" : "text-gray-900"
}`}
>
{option.label}
</h3>
{option.description && (
<div
className={`text-xs ${
value === option.value ? "text-blue-500" : "text-gray-500"
}`}
>
{option.description}
</div>
)}
</div>
))}
</div>
);
};
export default RadioCard;

View File

@@ -0,0 +1,239 @@
import { Input, Button, Select, Tag, Segmented, DatePicker } from "antd";
import {
BarsOutlined,
AppstoreOutlined,
SearchOutlined,
ReloadOutlined,
} from "@ant-design/icons";
import { useEffect, useState } from "react";
interface FilterOption {
key: string;
label: string;
mode?: "tags" | "multiple";
options: { label: string; value: string }[];
}
interface SearchControlsProps {
searchTerm: string;
onSearchChange: (value: string) => void;
searchPlaceholder?: string;
// Filter props
filters?: FilterOption[];
selectedFilters?: Record<string, string[]>;
onFiltersChange?: (filters: Record<string, string[]>) => void;
onClearFilters?: () => void;
// Date range props
dateRange?: [Date | null, Date | null] | null;
onDateChange?: (dates: [Date | null, Date | null] | null) => void;
// Reload props
onReload?: () => void;
// View props
viewMode?: "card" | "list";
onViewModeChange?: (mode: "card" | "list") => void;
// Control visibility
showFilters?: boolean;
showSort?: boolean;
showViewToggle?: boolean;
showReload?: boolean;
showDatePicker?: boolean;
// Styling
className?: string;
}
export function SearchControls({
viewMode,
className,
searchTerm,
showFilters = true,
showViewToggle = true,
searchPlaceholder = "搜索...",
filters = [],
dateRange,
showDatePicker = false,
showReload = true,
onReload,
onDateChange,
onSearchChange,
onFiltersChange,
onViewModeChange,
onClearFilters,
}: SearchControlsProps) {
const [selectedFilters, setSelectedFilters] = useState<{
[key: string]: string[];
}>({});
const filtersMap: Record<string, FilterOption> = filters.reduce(
(prev, cur) => ({ ...prev, [cur.key]: cur }),
{}
);
// select change
const handleFilterChange = (filterKey: string, value: string) => {
const filteredValues = {
...selectedFilters,
[filterKey]: !value ? [] : [value],
};
setSelectedFilters(filteredValues);
};
// 清除已选筛选
const handleClearFilter = (filterKey: string, value: string | string[]) => {
const isMultiple = filtersMap[filterKey]?.mode === "multiple";
if (!isMultiple) {
setSelectedFilters({
...selectedFilters,
[filterKey]: [],
});
} else {
const currentValues = selectedFilters[filterKey]?.[0] || [];
const newValues = currentValues.filter((v) => v !== value);
setSelectedFilters({
...selectedFilters,
[filterKey]: [newValues],
});
}
};
const handleClearAllFilters = () => {
setSelectedFilters({});
onClearFilters?.();
};
const hasActiveFilters = Object.values(selectedFilters).some(
(values) => values?.[0]?.length > 0
);
useEffect(() => {
if (Object.keys(selectedFilters).length === 0) return;
onFiltersChange?.(selectedFilters);
}, [selectedFilters]);
return (
<div className={className}>
<div className="flex items-center justify-between gap-8">
{/* Left side - Search and Filters */}
<div className="flex items-center gap-2 flex-1">
{/* Search */}
<div className="relative flex-1">
<Input
allowClear
placeholder={searchPlaceholder}
value={searchTerm}
onChange={(e) => onSearchChange(e.target.value)}
prefix={<SearchOutlined className="w-4 h-4 text-gray-400" />}
/>
</div>
{/* Filters */}
{showFilters && filters.length > 0 && (
<div className="flex items-center gap-2">
{filters.map((filter: FilterOption) => (
<Select
maxTagCount="responsive"
mode={filter.mode}
key={filter.key}
placeholder={filter.label}
value={selectedFilters[filter.key]?.[0] || undefined}
onChange={(value) => handleFilterChange(filter.key, value)}
style={{ width: 144 }}
allowClear
>
{filter.options.map((option) => (
<Select.Option key={option.value} value={option.value}>
{option.label}
</Select.Option>
))}
</Select>
))}
</div>
)}
</div>
{showDatePicker && (
<DatePicker.RangePicker
value={dateRange as any}
onChange={onDateChange}
style={{ width: 260 }}
allowClear
placeholder={["开始时间", "结束时间"]}
/>
)}
{/* Right side */}
<div className="flex items-center gap-2">
{showViewToggle && onViewModeChange && (
<Segmented
options={[
{ value: "list", icon: <BarsOutlined /> },
{ value: "card", icon: <AppstoreOutlined /> },
]}
value={viewMode}
onChange={(value) => onViewModeChange(value as "list" | "card")}
/>
)}
{showReload && (
<Button
icon={<ReloadOutlined />}
onClick={() => onReload?.()}
></Button>
)}
</div>
</div>
{/* Active Filters Display */}
{hasActiveFilters && (
<div className="mt-4 pt-4 border-t border-gray-200">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2 flex-wrap flex-1">
<span className="text-sm font-medium text-gray-700">
:
</span>
{Object.entries(selectedFilters).map(([filterKey, values]) =>
values.map((value) => {
const filter = filtersMap[filterKey];
const getLabeledValue = (item: string) => {
const option = filter?.options.find(
(o) => o.value === item
);
return (
<Tag
key={`${filterKey}-${item}`}
closable
onClose={() => handleClearFilter(filterKey, item)}
color="blue"
>
{filter?.label}: {option?.label || item}
</Tag>
);
};
return Array.isArray(value)
? value.map((item) => getLabeledValue(item))
: getLabeledValue(value);
})
)}
</div>
{/* Clear all filters button on the right */}
<Button
type="text"
size="small"
onClick={handleClearAllFilters}
className="text-gray-500 hover:text-gray-700"
>
</Button>
</div>
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,149 @@
import React, { useEffect, useRef, useState } from "react";
import { PlusOutlined } from "@ant-design/icons";
import type { InputRef } from "antd";
import { Flex, Input, Tag, theme, Tooltip } from "antd";
const tagInputStyle: React.CSSProperties = {
width: 64,
height: 22,
marginInlineEnd: 8,
verticalAlign: "top",
};
interface TagListProps {
tags: string[];
setTags: (tags: string[]) => void;
onDelete?: (tag: string) => void;
onAdd?: (tag: string) => void;
onEdit?: (oldTag: string, newTag: string) => void;
}
const TagList: React.FC<TagListProps> = ({
tags,
setTags,
onDelete,
onAdd,
onEdit,
}) => {
const { token } = theme.useToken();
const [inputVisible, setInputVisible] = useState(false);
const [inputValue, setInputValue] = useState("");
const [editInputIndex, setEditInputIndex] = useState(-1);
const [editInputValue, setEditInputValue] = useState("");
const inputRef = useRef<InputRef>(null);
const editInputRef = useRef<InputRef>(null);
useEffect(() => {
if (inputVisible) {
inputRef.current?.focus();
}
}, [inputVisible]);
useEffect(() => {
editInputRef.current?.focus();
}, [editInputValue]);
const handleClose = (removedTag: string) => {
const newTags = tags.filter((tag) => tag !== removedTag);
setTags(newTags);
onDelete?.(removedTag);
};
const showInput = () => {
setInputVisible(true);
};
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setInputValue(e.target.value);
};
const handleInputConfirm = () => {
if (inputValue && !tags.includes(inputValue)) {
setTags([...tags, inputValue]);
onAdd?.(inputValue);
}
setInputVisible(false);
setInputValue("");
};
const handleEditInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setEditInputValue(e.target.value);
};
const handleEditInputConfirm = () => {
const newTags = [...tags];
newTags[editInputIndex] = editInputValue;
setTags(newTags);
onEdit?.(tags[editInputIndex], editInputValue);
setEditInputIndex(-1);
setEditInputValue("");
};
const tagPlusStyle: React.CSSProperties = {
height: 22,
background: token.colorBgContainer,
borderStyle: "dashed",
};
return (
<Flex gap="4px 0" wrap>
{tags.map<React.ReactNode>((tag, index) => {
if (editInputIndex === index) {
return (
<Input
ref={editInputRef}
key={tag}
size="small"
style={tagInputStyle}
value={editInputValue}
onChange={handleEditInputChange}
onBlur={handleEditInputConfirm}
onPressEnter={handleEditInputConfirm}
/>
);
}
const isLongTag = tag.length > 20;
const tagElem = (
<Tag key={tag} onClose={() => handleClose(tag)} closable>
<span
onDoubleClick={(e) => {
if (index !== 0) {
setEditInputIndex(index);
setEditInputValue(tag);
e.preventDefault();
}
}}
>
{isLongTag ? `${tag.slice(0, 20)}...` : tag}
</span>
</Tag>
);
return isLongTag ? (
<Tooltip title={tag} key={tag}>
{tagElem}
</Tooltip>
) : (
tagElem
);
})}
{inputVisible ? (
<Input
ref={inputRef}
type="text"
size="small"
style={tagInputStyle}
value={inputValue}
onChange={handleInputChange}
onBlur={handleInputConfirm}
onPressEnter={handleInputConfirm}
/>
) : (
<Tag style={tagPlusStyle} icon={<PlusOutlined />} onClick={showInput}>
</Tag>
)}
</Flex>
);
};
export default TagList;

View File

@@ -0,0 +1,271 @@
import React, { useEffect, useState } from "react";
import { Drawer, Input, Button, App } from "antd";
import { PlusOutlined } from "@ant-design/icons";
import { Edit, Save, TagIcon, X, Trash } from "lucide-react";
import { TagItem } from "@/pages/DataManagement/dataset.model";
interface CustomTagProps {
isEditable?: boolean;
tag: { id: number; name: string };
editingTag?: string | null;
editingTagValue?: string;
setEditingTag?: React.Dispatch<React.SetStateAction<string | null>>;
setEditingTagValue?: React.Dispatch<React.SetStateAction<string>>;
handleEditTag?: (tag: { id: number; name: string }, value: string) => void;
handleCancelEdit?: (tag: { id: number; name: string }) => void;
handleDeleteTag?: (tag: { id: number; name: string }) => void;
}
function CustomTag({
isEditable = false,
tag,
editingTag,
editingTagValue,
setEditingTag,
setEditingTagValue,
handleEditTag,
handleCancelEdit,
handleDeleteTag,
}: CustomTagProps) {
return (
<div
key={tag.id}
className="flex items-center justify-between px-4 py-2 border border-gray-100 rounded-md hover:bg-gray-50"
>
{editingTag?.id === tag.id ? (
<div className="flex gap-2 flex-1">
<Input
value={editingTagValue}
onChange={(e) => setEditingTagValue?.(e.target.value)}
onKeyPress={(e) => {
if (e.key === "Enter") {
handleEditTag?.(tag, editingTagValue);
}
if (e.key === "Escape") {
setEditingTag?.(null);
setEditingTagValue?.("");
}
}}
className="h-6 text-sm"
autoFocus
/>
<Button
onClick={() => handleEditTag(tag, editingTagValue)}
type="link"
size="small"
icon={<Save className="w-3 h-3" />}
/>
<Button
danger
type="text"
size="small"
onClick={() => handleCancelEdit?.(tag)}
icon={<X className="w-3 h-3" />}
/>
</div>
) : (
<>
<span className="text-sm">{tag.name}</span>
{isEditable && (
<div className="flex gap-1">
<Button
size="small"
type="text"
onClick={() => {
setEditingTag?.(tag);
setEditingTagValue?.(tag.name);
}}
icon={<Edit className="w-3 h-3" />}
/>
<Button
danger
type="text"
size="small"
onClick={() => handleDeleteTag?.(tag)}
icon={<Trash className="w-3 h-3" />}
/>
</div>
)}
</>
)}
</div>
);
}
export const mockPreparedTags = [
{ id: "1", name: "重要" },
{ id: "2", name: "待处理" },
{ id: "3", name: "已完成" },
{ id: "4", name: "审核中" },
{ id: "5", name: "高优先级" },
{ id: "6", name: "低优先级" },
{ id: "7", name: "客户A" },
{ id: "8", name: "客户B" },
];
const TagManager: React.FC = ({
onFetch,
onCreate,
onDelete,
onUpdate,
}: {
onFetch: () => Promise<any>;
onCreate: (tag: Pick<TagItem, "name">) => Promise<{ ok: boolean }>;
onDelete: (tagId: number) => Promise<{ ok: boolean }>;
onUpdate: (oldTagId: number, newTag: string) => Promise<{ ok: boolean }>;
}) => {
const [showTagManager, setShowTagManager] = useState(false);
const { message } = App.useApp();
const [tags, setTags] = useState<{ id: number; name: string }[]>([]);
const [newTag, setNewTag] = useState("");
const [editingTag, setEditingTag] = useState<string | null>(null);
const [editingTagValue, setEditingTagValue] = useState("");
// 预置标签
const [preparedTags, setPreparedTags] = useState(mockPreparedTags);
// 获取标签列表
const fetchTags = async () => {
if (!onFetch) return;
try {
const { data } = await onFetch?.();
setTags(data || []);
} catch (e) {
message.error("获取标签失败");
}
};
// 添加标签
const addTag = async (tag: string) => {
try {
await onCreate?.({
name: tag,
});
fetchTags();
message.success("标签添加成功");
} catch (error) {
message.error("添加标签失败");
}
};
// 删除标签
const deleteTag = async (tag: TagItem) => {
try {
await onDelete?.(tag.id);
fetchTags();
message.success("标签删除成功");
} catch (error) {
message.error("删除标签失败");
}
};
const updateTag = async (oldTag: TagItem, newTag: string) => {
try {
await onUpdate?.(oldTag.id, { ...oldTag, name: newTag });
fetchTags();
message.success("标签更新成功");
} catch (error) {
message.error("更新标签失败");
}
};
const handleCreateNewTag = () => {
if (newTag.trim()) {
addTag(newTag.trim());
setNewTag("");
}
};
const handleEditTag = (tag: TagItem, value: string) => {
if (value.trim()) {
updateTag(tag, value.trim());
setEditingTag(null);
setEditingTagValue("");
}
};
const handleCancelEdit = (tag: string) => {
setEditingTag(null);
setEditingTagValue("");
};
const handleDeleteTag = (tag: TagItem) => {
deleteTag(tag);
setEditingTag(null);
setEditingTagValue("");
};
useEffect(() => {
if (showTagManager) fetchTags();
}, [showTagManager]);
return (
<>
<Button
icon={<TagIcon className="w-4 h-4 mr-2" />}
onClick={() => setShowTagManager(true)}
>
</Button>
<Drawer
open={showTagManager}
onClose={() => setShowTagManager(false)}
title="标签管理"
width={500}
>
<div className="space-y-4">
{/* Add New Tag */}
<div className="space-y-2">
<div className="flex gap-2">
<Input
placeholder="输入标签名称..."
value={newTag}
allowClear
onChange={(e) => setNewTag(e.target.value)}
onKeyPress={(e) => {
if (e.key === "Enter") {
addTag(e.target.value);
}
}}
/>
<Button
type="primary"
onClick={handleCreateNewTag}
disabled={!newTag.trim()}
icon={<PlusOutlined />}
>
</Button>
</div>
</div>
<h2 className="font-large font-bold w-full"></h2>
<div className="grid grid-cols-2 gap-2">
{preparedTags.length > 0 &&
preparedTags.map((tag) => <CustomTag key={tag.id} tag={tag} />)}
</div>
<h2 className="font-large font-bold w-full"></h2>
<div className="grid grid-cols-2 gap-2 mt-4">
{tags.map((tag) => (
<CustomTag
isEditable
key={tag.id}
tag={tag}
editingTag={editingTag}
editingTagValue={editingTagValue}
setEditingTag={setEditingTag}
setEditingTagValue={setEditingTagValue}
handleEditTag={handleEditTag}
handleCancelEdit={handleCancelEdit}
handleDeleteTag={handleDeleteTag}
/>
))}
</div>
</div>
</Drawer>
</>
);
};
export default TagManager;

View File

@@ -0,0 +1,162 @@
import { Button, Popover, Progress } from "antd";
import { Calendar, Clock, Play, Trash2, X } from "lucide-react";
interface TaskItem {
id: string;
name: string;
status: string;
progress: number;
scheduleConfig: {
type: string;
cronExpression?: string;
executionCount?: number;
maxExecutions?: number;
};
nextExecution?: string;
importConfig: {
source?: string;
};
createdAt: string;
}
export default function TaskPopover() {
const tasks: TaskItem[] = [
{
id: "1",
name: "导入客户数据",
status: "importing",
progress: 65,
scheduleConfig: {
type: "manual",
},
importConfig: {
source: "local",
},
createdAt: "2025-07-29 14:23",
},
{
id: "2",
name: "定时同步订单",
status: "waiting",
progress: 0,
scheduleConfig: {
type: "scheduled",
cronExpression: "0 0 * * *",
executionCount: 3,
maxExecutions: 10,
},
nextExecution: "2025-07-31 00:00",
importConfig: {
source: "api",
},
createdAt: "2025-07-28 09:10",
},
{
id: "3",
name: "清理历史日志",
status: "finished",
progress: 100,
scheduleConfig: {
type: "manual",
},
importConfig: {
source: "system",
},
createdAt: "2025-07-27 17:45",
},
];
return (
<Popover
placement="topLeft"
content={
<div className="w-[500px]">
<div className="flex items-center justify-between">
<h3 className="font-medium text-gray-900"></h3>
<Button type="text" className="h-6 w-6 p-0">
<X className="w-4 h-4 text-black-400 hover:text-gray-500" />
</Button>
</div>
<div className="p-2">
{tasks.length === 0 ? (
<div className="text-center py-8 text-gray-500">
<Clock className="w-8 h-8 mx-auto mb-2 text-gray-300" />
<p className="text-sm"></p>
</div>
) : (
<div className="space-y-2">
{tasks.map((task) => (
<div
key={task.id}
className="p-3 border border-gray-100 rounded-lg hover:bg-gray-50"
>
<div className="space-y-2">
<div className="flex items-center justify-between">
<h4 className="font-medium text-sm truncate flex-1">
{task.name}
</h4>
<div className="flex items-center gap-2">
{task.status === "waiting" && (
<Button
className="h-6 w-6 p-0 text-blue-500 hover:text-blue-700"
title="立即执行"
>
<Play className="w-3 h-3" />
</Button>
)}
<Button className="h-6 w-6 p-0 text-gray-400 hover:text-red-500">
<Trash2 className="w-3 h-3" />
</Button>
</div>
</div>
{task.status === "importing" && (
<div className="space-y-1">
<div className="flex justify-between text-xs text-gray-500">
<span></span>
<span>{Math.round(task.progress)}%</span>
</div>
<Progress percent={task.progress} />
</div>
)}
{/* Schedule Information */}
{task.scheduleConfig.type === "scheduled" && (
<div className="text-xs text-gray-500 bg-gray-50 p-2 rounded">
<div className="flex items-center gap-1 mb-1">
<Calendar className="w-3 h-3" />
<span className="font-medium"></span>
</div>
<div>Cron: {task.scheduleConfig.cronExpression}</div>
{task.nextExecution && (
<div>: {task.nextExecution}</div>
)}
<div>
: {task.scheduleConfig.executionCount || 0}/
{task.scheduleConfig.maxExecutions || 10}
</div>
</div>
)}
<div className="flex items-center justify-between text-xs text-gray-400">
<span>
{task.importConfig.source === "local"
? "本地上传"
: task.importConfig.source || "未知来源"}
</span>
<span>{task.createdAt}</span>
</div>
</div>
</div>
))}
</div>
)}
</div>
</div>
}
>
<Button block></Button>
</Popover>
);
}

View File

@@ -0,0 +1,69 @@
import { useEffect, useRef, useState } from "react";
const TopLoadingBar = () => {
const [isVisible, setIsVisible] = useState(false);
const [progress, setProgress] = useState(0);
const intervalRef = useRef(null);
useEffect(() => {
// 监听全局事件
const handleShow = () => {
setIsVisible(true);
setProgress(0);
// 清除可能存在的旧interval
if (intervalRef.current) {
clearInterval(intervalRef.current);
}
// 模拟进度
let currentProgress = 0;
intervalRef.current = setInterval(() => {
currentProgress += Math.random() * 10;
if (currentProgress >= 90) {
clearInterval(intervalRef.current);
}
setProgress(currentProgress);
}, 200);
};
const handleHide = () => {
// 清除进度interval
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
setProgress(100);
setTimeout(() => {
setIsVisible(false);
setProgress(0);
}, 300);
};
// 添加全局事件监听器
window.addEventListener("loading:show", handleShow);
window.addEventListener("loading:hide", handleHide);
return () => {
// 组件卸载时清理
if (intervalRef.current) {
clearInterval(intervalRef.current);
}
window.removeEventListener("loading:show", handleShow);
window.removeEventListener("loading:hide", handleHide);
};
}, []);
if (!isVisible) return null;
return (
<div className="top-loading-bar">
<div
className="loading-bar-progress"
style={{ width: `${progress}%` }}
></div>
</div>
);
};
export default TopLoadingBar;

View File

@@ -0,0 +1,17 @@
import { useEffect } from "react";
export function useDebouncedEffect(
cb: () => void,
deps: any[] = [],
delay: number = 300
) {
useEffect(() => {
const handler = setTimeout(() => {
cb();
}, delay);
return () => {
clearTimeout(handler);
};
}, [...(deps || []), delay]);
}

View File

@@ -0,0 +1,113 @@
// 首页数据获取
import { useState } from "react";
import { useDebouncedEffect } from "./useDebouncedEffect";
import Loading from "@/utils/loading";
import { App } from "antd";
export default function useFetchData<T>(
fetchFunc: (params?: any) => Promise<any>,
mapDataFunc: (data: any) => T = (data) => data as T
) {
const { message } = App.useApp();
// 表格数据
const [tableData, setTableData] = useState<T[]>([]);
// 设置加载状态
const [loading, setLoading] = useState(false);
// 搜索参数
const [searchParams, setSearchParams] = useState({
keyword: "",
filter: {
type: [] as string[],
status: [] as string[],
tags: [] as string[],
},
current: 1,
pageSize: 12,
});
// 分页配置
const [pagination, setPagination] = useState({
total: 0,
showSizeChanger: true,
pageSizeOptions: ["12", "24", "48"],
showTotal: (total: number) => `${total}`,
onChange: (current: number, pageSize?: number) => {
setSearchParams((prev) => ({
...prev,
current,
pageSize: pageSize || prev.pageSize,
}));
},
});
const handleFiltersChange = (searchFilters: { [key: string]: string[] }) => {
setSearchParams({
...searchParams,
current: 1,
filter: { ...searchParams.filter, ...searchFilters },
});
};
function getFirstOfArray(arr: string[]) {
if (!arr || arr.length === 0 || !Array.isArray(arr)) return undefined;
if (arr[0] === "all") return undefined;
return arr[0];
}
async function fetchData(extraParams = {}) {
const { keyword, filter, current, pageSize } = searchParams;
Loading.show();
setLoading(true);
try {
const { data } = await fetchFunc({
...filter,
...extraParams,
keyword,
type: getFirstOfArray(filter?.type) || undefined,
status: getFirstOfArray(filter?.status) || undefined,
tags: filter?.tags?.length ? filter.tags.join(",") : undefined,
page: current - 1,
size: pageSize,
});
setPagination((prev) => ({
...prev,
total: data?.totalElements || 0,
}));
let result = [];
if (mapDataFunc) {
result = data?.content.map(mapDataFunc) ?? [];
}
setTableData(result);
} catch (error) {
console.error(error)
message.error("数据获取失败,请稍后重试");
} finally {
Loading.hide();
setLoading(false);
}
}
useDebouncedEffect(
() => {
fetchData();
},
[searchParams],
searchParams?.keyword ? 500 : 0
);
return {
loading,
tableData,
pagination: {
...pagination,
current: searchParams.current,
pageSize: searchParams.pageSize,
},
searchParams,
setSearchParams,
setPagination,
handleFiltersChange,
fetchData,
};
}

View File

@@ -0,0 +1,52 @@
import { useCallback, useEffect, useState } from "react";
import { useNavigate } from "react-router";
// 自定义hook:页面离开前提示
export function useLeavePrompt(shouldPrompt: boolean) {
const navigate = useNavigate();
const [showPrompt, setShowPrompt] = useState(false);
const [nextPath, setNextPath] = useState<string | null>(null);
// 浏览器刷新/关闭
useEffect(() => {
const handler = (e: BeforeUnloadEvent) => {
if (shouldPrompt) {
e.preventDefault();
e.returnValue = "";
return "";
}
};
window.addEventListener("beforeunload", handler);
return () => window.removeEventListener("beforeunload", handler);
}, [shouldPrompt]);
// 路由切换拦截
useEffect(() => {
const unblock = (window as any).__REACT_ROUTER_DOM_HISTORY__?.block?.(
(tx: any) => {
if (shouldPrompt) {
setShowPrompt(true);
setNextPath(tx.location.pathname);
return false;
}
return true;
}
);
return () => {
if (unblock) unblock();
};
}, [shouldPrompt]);
const confirmLeave = useCallback(() => {
setShowPrompt(false);
if (nextPath) {
navigate(nextPath, { replace: true });
}
}, [nextPath, navigate]);
return {
showPrompt,
setShowPrompt,
confirmLeave,
};
}

View File

@@ -0,0 +1,18 @@
import { useMemo } from "react";
import { useLocation } from "react-router";
interface AnyObject {
[key: string]: any;
}
export function useSearchParams(): AnyObject {
const { search } = useLocation();
return useMemo(() => {
const urlParams = new URLSearchParams(search);
const params: AnyObject = {};
for (const [key, value] of urlParams.entries()) {
params[key] = value;
}
return params;
}, [search]);
}

View File

@@ -0,0 +1,20 @@
import { createStyles } from "antd-style";
const useStyle = createStyles(({ css, token }) => {
const { antCls } = token;
return {
customTable: css`
${antCls}-table {
${antCls}-table-container {
${antCls}-table-body, ${antCls}-table-content {
scrollbar-width: thin;
scrollbar-color: ${token.colorBorder} transparent;
scrollbar-gutter: stable;
}
}
}
`,
};
});
export default useStyle;

45
frontend/src/index.css Normal file
View File

@@ -0,0 +1,45 @@
@import "tailwindcss";
/* components/TopLoadingBar.css */
.top-loading-bar {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 3px;
background-color: transparent;
z-index: 9999;
overflow: hidden;
}
.loading-bar-progress {
height: 100%;
background: linear-gradient(90deg, #3498db, #2ecc71, #3498db);
background-size: 200% 100%;
animation: gradient-animation 2s linear infinite, width-animation 0.3s ease;
transition: width 0.3s ease;
}
@keyframes gradient-animation {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
@keyframes width-animation {
from {
transform: translateX(-100%);
}
to {
transform: translateX(0);
}
}
.show-task-popover {
opacity: 100%;
visibility: visible;
transform: translateX(0);
}

18
frontend/src/main.tsx Normal file
View File

@@ -0,0 +1,18 @@
import { StrictMode, Suspense } from "react";
import { createRoot } from "react-dom/client";
import { RouterProvider } from "react-router";
import router from "./routes/routes";
import { App as AntdApp, Spin } from "antd";
import "./index.css";
import TopLoadingBar from "./components/TopLoadingBar";
createRoot(document.getElementById("root")!).render(
<StrictMode>
<AntdApp>
<Suspense fallback={<Spin />}>
<TopLoadingBar />
<RouterProvider router={router} />
</Suspense>
</AntdApp>
</StrictMode>
);

View File

@@ -0,0 +1,330 @@
import { BarChart, Circle, Grid, ImageIcon, Layers, Maximize, MousePointer, Move, Square, Target, Crop, RotateCcw, FileText, Tag, Heart, HelpCircle, BookOpen, MessageSquare, Users, Zap, Globe, Scissors } from "lucide-react";
// Define the AnnotationTask type if not imported from elsewhere
interface AnnotationTask {
id: string
name: string
completed: string
completedCount: number
skippedCount: number
totalCount: number
annotators: Array<{
id: string
name: string
avatar?: string
}>
text: string
status: "completed" | "in_progress" | "pending" | "skipped"
project: string
type: "图像分类" | "文本分类" | "目标检测" | "NER" | "语音识别" | "视频分析"
datasetType: "text" | "image" | "video" | "audio"
progress: number
}
export const mockTasks: AnnotationTask[] = [
{
id: "12345678",
name: "图像分类标注任务",
completed: "2024年1月20日 20:40",
completedCount: 1,
skippedCount: 0,
totalCount: 2,
annotators: [
{ id: "1", name: "张三", avatar: "/placeholder-user.jpg" },
{ id: "2", name: "李四", avatar: "/placeholder-user.jpg" },
],
text: "对产品图像进行分类标注,包含10个类别",
status: "completed",
project: "图像分类",
type: "图像分类",
datasetType: "image",
progress: 100,
},
{
id: "12345679",
name: "文本情感分析标注",
completed: "2024年1月20日 20:40",
completedCount: 2,
skippedCount: 0,
totalCount: 2,
annotators: [
{ id: "1", name: "王五", avatar: "/placeholder-user.jpg" },
{ id: "2", name: "赵六", avatar: "/placeholder-user.jpg" },
],
text: "对用户评论进行情感倾向标注",
status: "completed",
project: "文本分类",
type: "文本分类",
datasetType: "text",
progress: 100,
},
{
id: "12345680",
name: "目标检测标注任务",
completed: "2024年1月20日 20:40",
completedCount: 1,
skippedCount: 0,
totalCount: 2,
annotators: [{ id: "1", name: "孙七", avatar: "/placeholder-user.jpg" }],
text: "对交通场景图像进行目标检测标注",
status: "in_progress",
project: "目标检测",
type: "目标检测",
datasetType: "image",
progress: 50,
},
{
id: "12345681",
name: "命名实体识别标注",
completed: "2024年1月20日 20:40",
completedCount: 1,
skippedCount: 0,
totalCount: 2,
annotators: [{ id: "1", name: "周八", avatar: "/placeholder-user.jpg" }],
text: "对新闻文本进行命名实体识别标注",
status: "in_progress",
project: "NER",
type: "NER",
datasetType: "text",
progress: 75,
},
{
id: "12345682",
name: "语音识别标注任务",
completed: "2024年1月20日 20:40",
completedCount: 1,
skippedCount: 0,
totalCount: 2,
annotators: [{ id: "1", name: "吴九", avatar: "/placeholder-user.jpg" }],
text: "对语音数据进行转录和标注",
status: "in_progress",
project: "语音识别",
type: "语音识别",
datasetType: "audio",
progress: 25,
},
{
id: "12345683",
name: "视频动作识别标注",
completed: "2024年1月20日 20:40",
completedCount: 0,
skippedCount: 2,
totalCount: 2,
annotators: [
{ id: "1", name: "陈十", avatar: "/placeholder-user.jpg" },
{ id: "2", name: "林十一", avatar: "/placeholder-user.jpg" },
],
text: "对视频中的人体动作进行识别和标注",
status: "skipped",
project: "视频分析",
type: "视频分析",
datasetType: "video",
progress: 0,
},
]
// Define the Template type
type Template = {
id: string;
name: string;
category: string;
description: string;
type: string;
preview?: string;
icon: React.ReactNode;
};
// 扩展的预制模板数据
export const mockTemplates: Template[] = [
// 计算机视觉模板
{
id: "cv-1",
name: "目标检测",
category: "Computer Vision",
description: "使用边界框标注图像中的目标对象",
type: "image",
preview: "/placeholder.svg?height=120&width=180&text=Object+Detection",
icon: <Square className="w-4 h-4" />,
},
{
id: "cv-2",
name: "语义分割(多边形)",
category: "Computer Vision",
description: "使用多边形精确标注图像中的区域",
type: "image",
preview: "/placeholder.svg?height=120&width=180&text=Polygon+Segmentation",
icon: <Layers className="w-4 h-4" />,
},
{
id: "cv-3",
name: "语义分割(掩码)",
category: "Computer Vision",
description: "使用像素级掩码标注图像区域",
type: "image",
preview: "/placeholder.svg?height=120&width=180&text=Mask+Segmentation",
icon: <Circle className="w-4 h-4" />,
},
{
id: "cv-4",
name: "关键点标注",
category: "Computer Vision",
description: "标注图像中的关键点位置",
type: "image",
preview: "/placeholder.svg?height=120&width=180&text=Keypoint+Labeling",
icon: <MousePointer className="w-4 h-4" />,
},
{
id: "cv-5",
name: "图像分类",
category: "Computer Vision",
description: "为整个图像分配类别标签",
type: "image",
preview: "/placeholder.svg?height=120&width=180&text=Image+Classification",
icon: <ImageIcon className="w-4 h-4" />,
},
{
id: "cv-6",
name: "实例分割",
category: "Computer Vision",
description: "区分同类别的不同实例对象",
type: "image",
preview: "/placeholder.svg?height=120&width=180&text=Instance+Segmentation",
icon: <Target className="w-4 h-4" />,
},
{
id: "cv-7",
name: "全景分割",
category: "Computer Vision",
description: "结合语义分割和实例分割的全景标注",
type: "image",
preview: "/placeholder.svg?height=120&width=180&text=Panoptic+Segmentation",
icon: <Grid className="w-4 h-4" />,
},
{
id: "cv-8",
name: "3D目标检测",
category: "Computer Vision",
description: "在3D空间中标注目标对象的位置和方向",
type: "image",
preview: "/placeholder.svg?height=120&width=180&text=3D+Object+Detection",
icon: <Maximize className="w-4 h-4" />,
},
{
id: "cv-9",
name: "图像配对",
category: "Computer Vision",
description: "标注图像之间的对应关系",
type: "image",
preview: "/placeholder.svg?height=120&width=180&text=Image+Matching",
icon: <Move className="w-4 h-4" />,
},
{
id: "cv-10",
name: "图像质量评估",
category: "Computer Vision",
description: "评估和标注图像质量等级",
type: "image",
preview: "/placeholder.svg?height=120&width=180&text=Quality+Assessment",
icon: <BarChart className="w-4 h-4" />,
},
{
id: "cv-11",
name: "图像裁剪标注",
category: "Computer Vision",
description: "标注图像中需要裁剪的区域",
type: "image",
preview: "/placeholder.svg?height=120&width=180&text=Image+Cropping",
icon: <Crop className="w-4 h-4" />,
},
{
id: "cv-12",
name: "图像旋转标注",
category: "Computer Vision",
description: "标注图像的正确方向角度",
type: "image",
preview: "/placeholder.svg?height=120&width=180&text=Image+Rotation",
icon: <RotateCcw className="w-4 h-4" />,
},
// 自然语言处理模板
{
id: "nlp-1",
name: "文本分类",
category: "Natural Language Processing",
description: "为文本分配类别标签",
type: "text",
icon: <FileText className="w-4 h-4" />,
},
{
id: "nlp-2",
name: "命名实体识别",
category: "Natural Language Processing",
description: "识别和标注文本中的实体",
type: "text",
icon: <Tag className="w-4 h-4" />,
},
{
id: "nlp-3",
name: "情感分析",
category: "Natural Language Processing",
description: "标注文本的情感倾向",
type: "text",
icon: <Heart className="w-4 h-4" />,
},
{
id: "nlp-4",
name: "问答标注",
category: "Natural Language Processing",
description: "标注问题和答案对",
type: "text",
icon: <HelpCircle className="w-4 h-4" />,
},
{
id: "nlp-5",
name: "文本摘要",
category: "Natural Language Processing",
description: "为长文本创建摘要标注",
type: "text",
icon: <BookOpen className="w-4 h-4" />,
},
{
id: "nlp-6",
name: "对话标注",
category: "Natural Language Processing",
description: "标注对话中的意图和实体",
type: "text",
icon: <MessageSquare className="w-4 h-4" />,
},
{
id: "nlp-7",
name: "关系抽取",
category: "Natural Language Processing",
description: "标注实体之间的关系",
type: "text",
icon: <Users className="w-4 h-4" />,
},
{
id: "nlp-8",
name: "文本相似度",
category: "Natural Language Processing",
description: "标注文本之间的相似度",
type: "text",
icon: <Zap className="w-4 h-4" />,
},
{
id: "nlp-9",
name: "语言检测",
category: "Natural Language Processing",
description: "识别和标注文本的语言类型",
type: "text",
icon: <Globe className="w-4 h-4" />,
},
{
id: "nlp-10",
name: "文本纠错",
category: "Natural Language Processing",
description: "标注文本中的错误并提供修正",
type: "text",
icon: <Scissors className="w-4 h-4" />,
},
]

View File

@@ -0,0 +1,56 @@
import {
DatabaseOutlined,
BarChartOutlined,
FileTextOutlined,
ThunderboltOutlined,
PictureOutlined,
CalculatorOutlined,
SwapOutlined,
} from "@ant-design/icons";
import { FileImage, FileText, Music, Repeat, Video } from "lucide-react";
// 模板类型选项
export const templateTypes = [
{
value: "text",
label: "文本",
icon: FileText,
description: "处理文本数据的清洗模板",
},
{
value: "image",
label: "图片",
icon: FileImage,
description: "处理图像数据的清洗模板",
},
{
value: "video",
label: "视频",
icon: Video,
description: "处理视频数据的清洗模板",
},
{
value: "audio",
label: "音频",
icon: Music,
description: "处理音频数据的清洗模板",
},
{
value: "image-to-text",
label: "图片转文本",
icon: Repeat,
description: "图像识别转文本的处理模板",
},
];
// 算子分类
export const OPERATOR_CATEGORIES = {
data: { name: "数据清洗", icon: <DatabaseOutlined />, color: "#1677ff" },
ml: { name: "机器学习", icon: <ThunderboltOutlined />, color: "#722ed1" },
vision: { name: "计算机视觉", icon: <PictureOutlined />, color: "#52c41a" },
nlp: { name: "自然语言处理", icon: <FileTextOutlined />, color: "#faad14" },
analysis: { name: "数据分析", icon: <BarChartOutlined />, color: "#f5222d" },
transform: { name: "数据转换", icon: <SwapOutlined />, color: "#13c2c2" },
io: { name: "输入输出", icon: <FileTextOutlined />, color: "#595959" },
math: { name: "数学计算", icon: <CalculatorOutlined />, color: "#fadb14" },
};

View File

@@ -0,0 +1,290 @@
// 预设评估维度配置
export const presetEvaluationDimensions: EvaluationDimension[] = [
{
id: "answer_relevance",
name: "回答相关性",
description: "评估回答内容是否针对问题,是否切中要点",
category: "accuracy",
isEnabled: true,
},
{
id: "content_quality",
name: "内容质量",
description: "评估内容的准确性、完整性和可读性",
category: "quality",
isEnabled: true,
},
{
id: "information_completeness",
name: "信息完整性",
description: "评估信息是否完整,无缺失关键内容",
category: "completeness",
isEnabled: true,
},
{
id: "language_fluency",
name: "语言流畅性",
description: "评估语言表达是否流畅自然",
category: "quality",
isEnabled: true,
},
{
id: "factual_accuracy",
name: "事实准确性",
description: "评估内容中事实信息的准确性",
category: "accuracy",
isEnabled: true,
},
]
export const sliceOperators: SliceOperator[] = [
{
id: "paragraph-split",
name: "段落分割",
description: "按段落自然分割文档",
type: "text",
icon: "📄",
params: { minLength: 50, maxLength: 1000 },
},
{
id: "sentence-split",
name: "句子分割",
description: "按句子边界分割文档",
type: "text",
icon: "📝",
params: { maxSentences: 5, overlap: 1 },
},
{
id: "semantic-split",
name: "语义分割",
description: "基于语义相似度智能分割",
type: "semantic",
icon: "🧠",
params: { threshold: 0.7, windowSize: 3 },
},
{
id: "length-split",
name: "长度分割",
description: "按固定字符长度分割",
type: "text",
icon: "📏",
params: { chunkSize: 512, overlap: 50 },
},
{
id: "structure-split",
name: "结构化分割",
description: "按文档结构(标题、章节)分割",
type: "structure",
icon: "🏗️",
params: { preserveHeaders: true, minSectionLength: 100 },
},
{
id: "table-extract",
name: "表格提取",
description: "提取并单独处理表格内容",
type: "structure",
icon: "📊",
params: { includeHeaders: true, mergeRows: false },
},
{
id: "code-extract",
name: "代码提取",
description: "识别并提取代码块",
type: "custom",
icon: "💻",
params: { languages: ["python", "javascript", "sql"], preserveIndentation: true },
},
{
id: "qa-extract",
name: "问答提取",
description: "自动识别问答格式内容",
type: "semantic",
icon: "❓",
params: { confidenceThreshold: 0.8, generateAnswers: true },
},
]
export const mockTasks: EvaluationTask[] = [
{
id: "1",
name: "客服对话数据质量评估",
datasetId: "1",
datasetName: "客服对话数据集",
evaluationType: "model",
status: "completed",
score: 85,
progress: 100,
createdAt: "2024-01-15 14:30",
completedAt: "2024-01-15 14:45",
description: "评估客服对话数据的质量,包括对话完整性、回复准确性等维度",
dimensions: ["answer_relevance", "content_quality", "information_completeness"],
customDimensions: [],
sliceConfig: {
threshold: 0.8,
sampleCount: 100,
method: "语义分割",
},
modelConfig: {
url: "https://api.openai.com/v1/chat/completions",
apiKey: "sk-***",
prompt: "请从数据质量、标签准确性、标注一致性三个维度评估这个客服对话数据集...",
temperature: 0.3,
maxTokens: 2000,
},
metrics: {
accuracy: 88,
completeness: 92,
consistency: 78,
relevance: 85,
},
issues: [
{ type: "重复数据", count: 23, severity: "medium" },
{ type: "格式错误", count: 5, severity: "high" },
{ type: "内容不完整", count: 12, severity: "low" },
],
},
{
id: "2",
name: "产品评论人工评估",
datasetId: "2",
datasetName: "产品评论数据集",
evaluationType: "manual",
status: "pending",
progress: 0,
createdAt: "2024-01-15 15:20",
description: "人工评估产品评论数据的情感标注准确性",
dimensions: ["content_quality", "factual_accuracy"],
customDimensions: [
{
id: "custom_1",
name: "情感极性准确性",
description: "评估情感标注的极性(正面/负面/中性)准确性",
category: "custom",
isCustom: true,
isEnabled: true,
},
],
sliceConfig: {
threshold: 0.7,
sampleCount: 50,
method: "段落分割",
},
metrics: {
accuracy: 0,
completeness: 0,
consistency: 0,
relevance: 0,
},
issues: [],
},
{
id: "3",
name: "新闻分类数据评估",
datasetId: "4",
datasetName: "新闻分类数据集",
evaluationType: "manual",
status: "running",
progress: 65,
createdAt: "2024-01-15 16:10",
description: "人工评估新闻分类数据集的标注质量",
dimensions: ["content_quality", "information_completeness", "factual_accuracy"],
customDimensions: [],
sliceConfig: {
threshold: 0.9,
sampleCount: 80,
method: "句子分割",
},
metrics: {
accuracy: 82,
completeness: 78,
consistency: 85,
relevance: 80,
},
issues: [{ type: "标注不一致", count: 15, severity: "medium" }],
},
]
// 模拟QA对数据
export const mockQAPairs: QAPair[] = [
{
id: "qa_1",
question: "这个产品的退货政策是什么?",
answer: "我们提供7天无理由退货服务,商品需要保持原包装完整。",
sliceId: "slice_1",
score: 4.5,
feedback: "回答准确且完整",
},
{
id: "qa_2",
question: "如何联系客服?",
answer: "您可以通过在线客服、电话400-123-4567或邮箱service@company.com联系我们。",
sliceId: "slice_2",
score: 5.0,
feedback: "提供了多种联系方式,非常全面",
},
{
id: "qa_3",
question: "配送时间需要多久?",
answer: "一般情况下,我们会在1-3个工作日内发货,配送时间根据地区不同为2-7天。",
sliceId: "slice_3",
score: 4.0,
feedback: "时间范围说明清楚",
},
]
// 评估维度模板配置
export const evaluationTemplates = {
dialogue_text: {
name: "对话文本评估",
dimensions: [
{
id: "answer_relevance",
name: "回答是否有针对性",
description: "评估回答内容是否针对问题,是否切中要点",
category: "accuracy" as const,
isEnabled: true,
},
{
id: "question_correctness",
name: "问题是否正确",
description: "评估问题表述是否清晰、准确、合理",
category: "quality" as const,
isEnabled: true,
},
{
id: "answer_independence",
name: "回答是否独立",
description: "评估回答是否独立完整,不依赖外部信息",
category: "completeness" as const,
isEnabled: true,
},
],
},
data_quality: {
name: "数据质量评估",
dimensions: [
{
id: "data_quality",
name: "数据质量",
description: "评估数据的整体质量,包括格式规范性、完整性等",
category: "quality" as const,
isEnabled: true,
},
{
id: "label_accuracy",
name: "标签准确性",
description: "评估数据标签的准确性和一致性",
category: "accuracy" as const,
isEnabled: true,
},
{
id: "data_completeness",
name: "数据完整性",
description: "评估数据集的完整性,是否存在缺失数据",
category: "completeness" as const,
isEnabled: true,
},
],
},
}

View File

@@ -0,0 +1,254 @@
export const mockChunks = Array.from({ length: 23 }, (_, i) => ({
id: i + 1,
content: `这是第 ${
i + 1
} 个文档分块的内容示例。在实际应用中,这里会显示从原始文档中提取和分割的具体文本内容。用户可以在这里查看和编辑分块的内容,确保知识库的质量和准确性。这个分块包含了重要的业务信息和技术细节,需要仔细维护以确保检索的准确性。`,
position: i + 1,
tokens: Math.floor(Math.random() * 200) + 100,
embedding: Array.from({ length: 1536 }, () => Math.random() - 0.5),
similarity: (Math.random() * 0.3 + 0.7).toFixed(3),
createdAt: "2024-01-22 10:35",
updatedAt: "2024-01-22 10:35",
vectorId: `vec_${i + 1}_${Math.random().toString(36).substr(2, 9)}`,
sliceOperator: ["semantic-split", "paragraph-split", "table-extract"][
Math.floor(Math.random() * 3)
],
parentChunkId: i > 0 ? Math.floor(Math.random() * i) + 1 : undefined,
metadata: {
source: "API文档.pdf",
page: Math.floor(i / 5) + 1,
section: `${Math.floor(i / 3) + 1}`,
},
}));
export const mockQAPairs = [
{
id: 1,
question: "什么是API文档的主要用途?",
answer:
"API文档的主要用途是为开发者提供详细的接口说明,包括请求参数、响应格式和使用示例.",
},
{
id: 2,
question: "如何正确使用这个API?",
answer:
"使用API时需要先获取访问令牌,然后按照文档中的格式发送请求,注意处理错误响应.",
},
];
export const sliceOperators: SliceOperator[] = [
{
id: "paragraph-split",
name: "段落分割",
description: "按段落自然分割文档",
type: "text",
icon: "📄",
params: { minLength: 50, maxLength: 1000 },
},
{
id: "sentence-split",
name: "句子分割",
description: "按句子边界分割文档",
type: "text",
icon: "📝",
params: { maxSentences: 5, overlap: 1 },
},
{
id: "semantic-split",
name: "语义分割",
description: "基于语义相似度智能分割",
type: "semantic",
icon: "🧠",
params: { threshold: 0.7, windowSize: 3 },
},
{
id: "length-split",
name: "长度分割",
description: "按固定字符长度分割",
type: "text",
icon: "📏",
params: { chunkSize: 512, overlap: 50 },
},
{
id: "structure-split",
name: "结构化分割",
description: "按文档结构(标题、章节)分割",
type: "structure",
icon: "🏗️",
params: { preserveHeaders: true, minSectionLength: 100 },
},
{
id: "table-extract",
name: "表格提取",
description: "提取并单独处理表格内容",
type: "structure",
icon: "📊",
params: { includeHeaders: true, mergeRows: false },
},
{
id: "code-extract",
name: "代码提取",
description: "识别并提取代码块",
type: "custom",
icon: "💻",
params: {
languages: ["python", "javascript", "sql"],
preserveIndentation: true,
},
},
{
id: "qa-extract",
name: "问答提取",
description: "自动识别问答格式内容",
type: "semantic",
icon: "❓",
params: { confidenceThreshold: 0.8, generateAnswers: true },
},
];
export const vectorDatabases = [
{
id: "pinecone",
name: "Pinecone",
description: "云端向量数据库,高性能检索",
},
{
id: "weaviate",
name: "Weaviate",
description: "开源向量数据库,支持多模态",
},
{ id: "qdrant", name: "Qdrant", description: "高性能向量搜索引擎" },
{ id: "chroma", name: "ChromaDB", description: "轻量级向量数据库" },
{ id: "milvus", name: "Milvus", description: "分布式向量数据库" },
{ id: "faiss", name: "FAISS", description: "Facebook AI 相似性搜索库" },
];
export const mockKnowledgeBases: KnowledgeBase[] = [
{
id: 1,
name: "产品技术文档库",
description:
"包含所有产品相关的技术文档和API说明,支持多种格式文档的智能解析和向量化处理",
type: "unstructured",
status: "ready",
fileCount: 45,
chunkCount: 1250,
vectorCount: 1250,
size: "2.3 GB",
progress: 100,
createdAt: "2024-01-15",
lastUpdated: "2024-01-22",
vectorDatabase: "pinecone",
config: {
embeddingModel: "text-embedding-3-large",
llmModel: "gpt-4o",
chunkSize: 512,
overlap: 50,
sliceMethod: "semantic",
enableQA: true,
vectorDimension: 1536,
sliceOperators: ["semantic-split", "paragraph-split", "table-extract"],
},
files: [
{
id: 1,
name: "API文档.pdf",
type: "pdf",
size: "2.5 MB",
status: "completed",
chunkCount: 156,
progress: 100,
uploadedAt: "2024-01-15",
source: "upload",
vectorizationStatus: "completed",
},
{
id: 2,
name: "用户手册.docx",
type: "docx",
size: "1.8 MB",
status: "disabled",
chunkCount: 89,
progress: 65,
uploadedAt: "2024-01-22",
source: "dataset",
datasetId: "dataset-1",
vectorizationStatus: "failed",
},
],
vectorizationHistory: [
{
id: 1,
timestamp: "2024-01-22 14:30:00",
operation: "create",
fileId: 1,
fileName: "API文档.pdf",
chunksProcessed: 156,
vectorsGenerated: 156,
status: "success",
duration: "2m 15s",
config: {
embeddingModel: "text-embedding-3-large",
chunkSize: 512,
sliceMethod: "semantic",
},
},
{
id: 2,
timestamp: "2024-01-22 15:45:00",
operation: "update",
fileId: 2,
fileName: "用户手册.docx",
chunksProcessed: 89,
vectorsGenerated: 0,
status: "failed",
duration: "0m 45s",
config: {
embeddingModel: "text-embedding-3-large",
chunkSize: 512,
sliceMethod: "semantic",
},
error: "向量化服务连接超时",
},
],
},
{
id: 2,
name: "FAQ结构化知识库",
description: "客服常见问题的结构化问答对,支持快速检索和智能匹配",
type: "structured",
status: "vectorizing",
fileCount: 12,
chunkCount: 890,
vectorCount: 750,
size: "156 MB",
progress: 75,
createdAt: "2024-01-20",
lastUpdated: "2024-01-23",
vectorDatabase: "weaviate",
config: {
embeddingModel: "text-embedding-ada-002",
chunkSize: 256,
overlap: 0,
sliceMethod: "paragraph",
enableQA: false,
vectorDimension: 1536,
sliceOperators: ["qa-extract", "paragraph-split"],
},
files: [
{
id: 3,
name: "FAQ模板.xlsx",
type: "xlsx",
size: "450 KB",
status: "vectorizing",
chunkCount: 234,
progress: 75,
uploadedAt: "2024-01-20",
source: "upload",
vectorizationStatus: "processing",
},
],
vectorizationHistory: [],
},
];

View File

@@ -0,0 +1,149 @@
const { addMockPrefix } = require("./mock-core/util.cjs");
const MockAPI = {
// 数据归集接口
queryTasksUsingPost: "/data-collection/tasks", // 获取数据源任务列表
createTaskUsingPost: "/data-collection/tasks/create", // 创建数据源任务
queryTaskByIdUsingGet: "/data-collection/tasks/:id", // 根据ID获取数据源任务详情
updateTaskByIdUsingPut: "/data-collection/tasks/:id", // 更新数据源任务
deleteTaskByIdUsingDelete: "/data-collection/tasks/:id", // 删除数据源任务
executeTaskByIdUsingPost: "/data-collection/tasks/:id/execute", // 执行数据源任务
stopTaskByIdUsingPost: "/data-collection/tasks/:id/stop", // 停止数据源任务
queryExecutionLogUsingPost: "/data-collection/executions", // 获取任务执行日志
queryExecutionLogByIdUsingGet: "/data-collection/executions/:id", // 获取任务执行日志详情
queryCollectionStatisticsUsingGet: "/data-collection/monitor/statistics", // 获取数据归集统计信息
// 数据管理接口
queryDatasetsUsingGet: "/data-management/datasets", // 获取数据集列表
createDatasetUsingPost: "/data-management/datasets", // 创建数据集
queryDatasetByIdUsingGet: "/data-management/datasets/:id", // 根据ID获取数据集详情
updateDatasetByIdUsingPut: "/data-management/datasets/:id", // 更新数据集
deleteDatasetByIdUsingDelete: "/data-management/datasets/:id", // 删除数据集
queryFilesUsingGet: "/data-management/datasets/:id/files", // 获取数据集文件列表
uploadFileUsingPost: "/data-management/datasets/:id/files", // 添加数据集文件
queryFileByIdUsingGet: "/data-management/datasets/:id/files/:fileId", // 获取数据集文件详情
deleteFileByIdUsingDelete: "/data-management/datasets/:id/files/:fileId", // 删除数据集文件
downloadFileByIdUsingGet:
"/data-management/datasets/:id/files/:fileId/download", // 下载文件
queryDatasetTypesUsingGet: "/data-management/dataset-types", // 获取数据集类型列表
queryTagsUsingGet: "/data-management/tags", // 获取数据集标签列表
createTagUsingPost: "/data-management/tags", // 创建数据集标签
updateTagUsingPost: "/data-management/tags", // 更新数据集标签
deleteTagUsingPost: "/data-management/tags", // 删除数据集标签
queryDatasetStatisticsUsingGet: "/data-management/datasets/statistics", // 获取数据集统计信息
preUploadFileUsingPost: "/data-management/datasets/:id/upload/pre-upload", // 预上传文件
cancelUploadUsingPut: "/data-management/datasets/upload/cancel-upload/:id", // 取消上传
uploadFileChunkUsingPost: "/data-management/datasets/:id/upload/chunk", // 上传切片
// 数据清洗接口
queryCleaningTasksUsingGet: "/cleaning/tasks", // 获取清洗任务列表
createCleaningTaskUsingPost: "/cleaning/tasks", // 创建清洗任务
queryCleaningTaskByIdUsingGet: "/cleaning/tasks/:taskId", // 根据ID获取清洗任务详情
deleteCleaningTaskByIdUsingDelete: "/cleaning/tasks/:taskId", // 删除清洗任务
executeCleaningTaskUsingPost: "/cleaning/tasks/:taskId/execute", // 执行清洗任务
stopCleaningTaskUsingPost: "/cleaning/tasks/:taskId/stop", // 停止清洗任务
queryCleaningTemplatesUsingGet: "/cleaning/templates", // 获取清洗模板列表
createCleaningTemplateUsingPost: "/cleaning/templates", // 创建清洗模板
queryCleaningTemplateByIdUsingGet: "/cleaning/templates/:templateId", // 根据ID获取清洗模板详情
updateCleaningTemplateByIdUsingPut: "/cleaning/templates/:templateId", // 根据ID更新清洗模板详情
deleteCleaningTemplateByIdUsingDelete: "/cleaning/templates/:templateId", // 删除清洗模板
// 数据标注接口
queryAnnotationTasksUsingGet: "/project/mappings/list", // 获取标注任务列表
createAnnotationTaskUsingPost: "/project/create", // 创建标注任务
syncAnnotationTaskByIdUsingPost: "/project/sync", // 同步标注任务
deleteAnnotationTaskByIdUsingDelete: "/project/mappings", // 删除标注任务
queryAnnotationTaskByIdUsingGet: "/annotation/tasks/:taskId", // 根据ID获取标注任务详情
executeAnnotationTaskByIdUsingPost: "/annotation/tasks/:taskId/execute", // 执行标注任务
stopAnnotationTaskByIdUsingPost: "/annotation/tasks/:taskId/stop", // 停止标注任务
queryAnnotationDataUsingGet: "/annotation/data", // 获取标注数据列表
submitAnnotationUsingPost: "/annotation/submit/:id", // 提交标注
updateAnnotationUsingPut: "/annotation/update/:id", // 根据ID更新标注
deleteAnnotationUsingDelete: "/annotation/delete/:id", // 根据ID删除标注
startAnnotationTaskUsingPost: "/annotation/start/:taskId", // 开始标注任务
pauseAnnotationTaskUsingPost: "/annotation/pause/:taskId", // 暂停标注任务
resumeAnnotationTaskUsingPost: "/annotation/resume/:taskId", // 恢复标注任务
completeAnnotationTaskUsingPost: "/annotation/complete/:taskId", // 完成标注任务
getAnnotationTaskStatisticsUsingGet: "/annotation/statistics/:taskId", // 获取标注任务统计信息
getAnnotationStatisticsUsingGet: "/annotation/statistics", // 获取标注统计信息
queryAnnotationTemplatesUsingGet: "/annotation/templates", // 获取标注模板列表
createAnnotationTemplateUsingPost: "/annotation/templates", // 创建标注模板
queryAnnotationTemplateByIdUsingGet: "/annotation/templates/:templateId", // 根据ID获取标注模板详情
queryAnnotatorsUsingGet: "/annotation/annotators", // 获取标注者列表
assignAnnotatorUsingPost: "/annotation/annotators/:annotatorId", // 分配标注者
// 数据合成接口
querySynthesisJobsUsingGet: "/synthesis/jobs", // 获取合成任务列表
createSynthesisJobUsingPost: "/synthesis/jobs/create", // 创建合成任务
querySynthesisJobByIdUsingGet: "/synthesis/jobs/:jobId", // 根据ID获取合成任务详情
updateSynthesisJobByIdUsingPut: "/synthesis/jobs/:jobId", // 更新合成任务
deleteSynthesisJobByIdUsingDelete: "/synthesis/jobs/:jobId", // 删除合成任务
executeSynthesisJobUsingPost: "/synthesis/jobs/execute/:jobId", // 执行合成任务
stopSynthesisJobByIdUsingPost: "/synthesis/jobs/stop/:jobId", // 停止合成任务
querySynthesisTemplatesUsingGet: "/synthesis/templates", // 获取合成模板列表
createSynthesisTemplateUsingPost: "/synthesis/templates/create", // 创建合成模板
querySynthesisTemplateByIdUsingGet: "/synthesis/templates/:templateId", // 根据ID获取合成模板详情
updateSynthesisTemplateByIdUsingPut: "/synthesis/templates/:templateId", // 更新合成模板
deleteSynthesisTemplateByIdUsingDelete: "/synthesis/templates/:templateId", // 删除合成模板
queryInstructionTemplatesUsingPost: "/synthesis/templates", // 获取指令模板列表
createInstructionTemplateUsingPost: "/synthesis/templates/create", // 创建指令模板
queryInstructionTemplateByIdUsingGet: "/synthesis/templates/:templateId", // 根据ID获取指令模板详情
deleteInstructionTemplateByIdUsingDelete: "/synthesis/templates/:templateId", // 删除指令模板
instructionTuningUsingPost: "/synthesis/instruction-tuning", // 指令微调
cotDistillationUsingPost: "/synthesis/cot-distillation", // Cot蒸馏
queryOperatorsUsingPost: "/synthesis/operators", // 获取操作列表
// 数据评测接口
queryEvaluationTasksUsingPost: "/evaluation/tasks", // 获取评测任务列表
createEvaluationTaskUsingPost: "/evaluation/tasks/create", // 创建评测任务
queryEvaluationTaskByIdUsingGet: "/evaluation/tasks/:taskId", // 根据ID获取评测任务详情
updateEvaluationTaskByIdUsingPut: "/evaluation/tasks/:taskId", // 更新评测任务
deleteEvaluationTaskByIdUsingDelete: "/evaluation/tasks/:taskId", // 删除评测任务
executeEvaluationTaskByIdUsingPost: "/evaluation/tasks/:taskId/execute", // 执行评测任务
stopEvaluationTaskByIdUsingPost: "/evaluation/tasks/:taskId/stop", // 停止评测任务
queryEvaluationReportsUsingPost: "/evaluation/reports", // 获取评测报告列表
queryEvaluationReportByIdUsingGet: "/evaluation/reports/:reportId", // 根据ID获取评测报告详情
manualEvaluateUsingPost: "/evaluation/manual-evaluate", // 人工评测
queryEvaluationStatisticsUsingGet: "/evaluation/statistics", // 获取评测统计信息
evaluateDataQualityUsingPost: "/evaluation/data-quality", // 数据质量评测
getQualityEvaluationByIdUsingGet: "/evaluation/data-quality/:id", // 根据ID获取数据质量评测详情
evaluateCompatibilityUsingPost: "/evaluation/compatibility", // 兼容性评测
evaluateValueUsingPost: "/evaluation/value", // 价值评测
queryEvaluationReportsUsingGet: "/evaluation/reports", // 获取评测报告列表(简化版)
getEvaluationReportByIdUsingGet: "/evaluation/reports/:reportId", // 根据ID获取评测报告详情(简化版)
exportEvaluationReportUsingGet: "/evaluation/reports/:reportId/export", // 导出评测报告
batchEvaluationUsingPost: "/evaluation/batch-evaluate", // 批量评测
// 知识生成接口
queryKnowledgeBasesUsingPost: "/knowledge/bases", // 获取知识库列表
createKnowledgeBaseUsingPost: "/knowledge/bases/create", // 创建知识库
queryKnowledgeBaseByIdUsingGet: "/knowledge/bases/:baseId", // 根据ID获取知识库详情
updateKnowledgeBaseByIdUsingPut: "/knowledge/bases/:baseId", // 更新知识库
deleteKnowledgeBaseByIdUsingDelete: "/knowledge/bases/:baseId", // 删除知识库
queryKnowledgeGenerationTasksUsingPost: "/knowledge/tasks", // 获取知识生成任务列表
createKnowledgeGenerationTaskUsingPost: "/knowledge/tasks/create", // 创建知识生成任务
queryKnowledgeGenerationTaskByIdUsingGet: "/knowledge/tasks/:taskId", // 根据ID获取知识生成任务详情
updateKnowledgeGenerationTaskByIdUsingPut: "/knowledge/tasks/:taskId", // 更新知识生成任务
deleteKnowledgeGenerationTaskByIdUsingDelete: "/knowledge/tasks/:taskId", // 删除知识生成任务
executeKnowledgeGenerationTaskByIdUsingPost:
"/knowledge/tasks/:taskId/execute", // 执行知识生成任务
stopKnowledgeGenerationTaskByIdUsingPost: "/knowledge/tasks/:taskId/stop", // 停止知识生成任务
queryKnowledgeStatisticsUsingGet: "/knowledge/statistics", // 获取知识生成
// 算子市场
queryOperatorsUsingPost: "/operators/list", // 获取算子列表
queryCategoryTreeUsingGet: "/categories/tree", // 获取算子分类树
queryOperatorByIdUsingGet: "/operators/:operatorId", // 根据ID获取算子详情
createOperatorUsingPost: "/operators/create", // 创建算子
updateOperatorByIdUsingPut: "/operators/:operatorId", // 更新算子
uploadOperatorUsingPost: "/operators/upload", // 上传算子
createLabelUsingPost: "/operators/labels", // 创建算子标签
queryLabelsUsingGet: "/labels", // 获取算子标签列表
deleteLabelsUsingDelete: "/labels", // 删除算子标签
updateLabelByIdUsingPut: "/labels/:labelId", // 更新算子标签
deleteOperatorByIdUsingDelete: "/operators/:operatorId", // 删除算子
publishOperatorUsingPost: "/operators/:operatorId/publish", // 发布算子
unpublishOperatorUsingPost: "/operators/:operatorId/unpublish", // 下架算子
};
module.exports = addMockPrefix("/api", MockAPI);

View File

@@ -0,0 +1,25 @@
const fs = require('fs');
function loadAllMockModules(router, pathDir) {
if (!fs.existsSync(pathDir)) {
throw new Error(`Mock directory ${pathDir} does not exist.`);
}
const files = fs.readdirSync(pathDir);
files.forEach(file => {
const filePath = `${pathDir}/${file}`;
if(fs.lstatSync(filePath).isDirectory()) {
loadAllMockModules(router, filePath);
} else {
let fileNameModule = file.replace('/\.js\b$/', '');
let module = require(`${pathDir}/${fileNameModule}`);
if(typeof module === 'function' && module.length === 1) {
module(router);
}
}
});
}
module.exports = {
loadAllMockModules,
};

View File

@@ -0,0 +1,63 @@
const path = require("path");
const Mock = require("mockjs");
const session = require("express-session");
const FileStore = require("session-file-store")(session);
const { isFunction } = require("lodash");
const argv = require("minimist")(process.argv.slice(2));
const isDev = (argv.env || "development") === "development";
const TOKEN_KEY = isDev ? "X-Auth-Token" : "X-Csrf-Token";
const setSessionUser = (req, getLoginInfo) => {
if (!isFunction(getLoginInfo)) {
throw new Error("getLoginInfo must be a function");
}
if (!req.session?.users) {
req.session.users = {};
}
let token = req.get(TOKEN_KEY);
const { users } = req.session;
if (!token || !users[token]) {
token = Mock.Random.guid().replace(/[^a-zA-Z0-9]/g, "");
const userInfo = getLoginInfo(req) || {};
users[token] = user;
}
return token;
};
const getSessionUser = (req) => {
const token = req.get(TOKEN_KEY);
if (token && req.session?.users) {
return req.session.users[token];
}
return null;
};
const genExpressSession = () => {
return session({
name: "demo.name",
secret: "demo.secret",
resave: true,
saveUninitialized: true,
cookie: {
maxAge: 60 * 60 * 1e3,
expires: new Date(Date.now() + 60 * 60 * 1e3),
}, // 1 hour
store: new FileStore({
path: path.join(__dirname, "../sessions"),
retries: 0,
keyFunction: (secret, sessionId) => {
return secret + sessionId;
},
}),
});
};
module.exports = {
setSessionUser,
getSessionUser,
genExpressSession,
};

View File

@@ -0,0 +1,30 @@
function log(message, type = "log", provided = 'console') {
const providedFn = globalThis[provided] || console;
if (providedFn && typeof providedFn[type] === 'function') {
const invokeMethod = providedFn[type ?? 'log'];
invokeMethod.call(providedFn, message);
}
}
function addMockPrefix(urlPrefix, api) {
const newMockApi = {};
Object.keys(api).map(apiKey=>{
newMockApi[apiKey] = urlPrefix + api[apiKey];
});
return new Proxy(newMockApi, {
get(target, prop) {
if (prop in target) {
return target[prop];
} else {
throw new Error(`API ${String(prop)} is not defined.`);
}
}
})
}
module.exports = {
log,
addMockPrefix,
};

View File

@@ -0,0 +1,13 @@
const errorHandle = (err, req, res, next) => {
if(res.headersSent) {
return next(err);
}
console.error('Server Error:', err.message);
res.status(500).json({
code: '500',
msg: 'Internal Server Error',
data: null,
});
};
module.exports = errorHandle;

View File

@@ -0,0 +1,11 @@
const setHeader = require('./set-header-middleware.cjs');
const strongMatch = require('./strong-match-middleware.cjs');
const sendJSON = require('./send-json-middleawre.cjs');
const errorHandle = require('./error-handle-middleware.cjs');
module.exports = {
setHeader,
strongMatch,
sendJSON,
errorHandle,
};

View File

@@ -0,0 +1,18 @@
const sendJSON = (req, res, next) => {
res.sendJSON = (
data = null,
{ code = '0', msg = 'success', statusCode = 200, timeout = 0 } = {}
) => {
const timer = setTimeout(() => {
res.status(statusCode).json({
code,
msg,
data,
});
clearTimeout(timer);
}, timeout);
};
next();
};
module.exports = sendJSON;

View File

@@ -0,0 +1,14 @@
const setHeader = (req, res, next) => {
res.set({
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET,POST,PUT,DELETE,OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
'Content-Security-Policy': "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src *; font-src 'self';",
'X-Content-Type-Options': 'nosniff',
'X-Frame-Options': 'SAMEORIGIN',
'X-XSS-Protection': '1; mode=block',
});
next();
};
module.exports = setHeader;

View File

@@ -0,0 +1,13 @@
const API = require('../mock-apis.cjs');
const strongMatch = (req, res, next) => {
res.strongMatch = () => {
const { url } = req;
const index = url.indexOf('?');
const targetUrl = index !== -1 ? url.substring(0, index) : url;
const isExistedUrl = Object.values(API).includes(targetUrl);
return isExistedUrl;
};
next();
};
module.exports = strongMatch;

View File

@@ -0,0 +1,618 @@
const Mock = require("mockjs");
const API = require("../mock-apis.cjs");
// 标注任务数据
function annotationTaskItem() {
return {
source_dataset_id: Mock.Random.guid().replace(/[^a-zA-Z0-9]/g, ""),
mapping_id: Mock.Random.guid().replace(/[^a-zA-Z0-9]/g, ""),
labelling_project_id: Mock.Random.guid().replace(/[^a-zA-Z0-9]/g, ""),
labelling_project_name: Mock.Random.ctitle(5, 20),
created_at: Mock.Random.datetime("yyyy-MM-dd HH:mm:ss"),
last_updated_at: Mock.Random.datetime("yyyy-MM-dd HH:mm:ss"),
deleted_at: Mock.Random.datetime("yyyy-MM-dd HH:mm:ss"),
// id: Mock.Random.guid().replace(/[^a-zA-Z0-9]/g, ""),
// name: Mock.Random.ctitle(5, 20),
// description: Mock.Random.csentence(5, 30),
// type: Mock.Random.pick([
// "TEXT_CLASSIFICATION",
// "NAMED_ENTITY_RECOGNITION",
// "OBJECT_DETECTION",
// "SEMANTIC_SEGMENTATION",
// ]),
// status: Mock.Random.pick(["PENDING", "IN_PROGRESS", "COMPLETED", "PAUSED"]),
// datasetId: Mock.Random.guid().replace(/[^a-zA-Z0-9]/g, ""),
// progress: Mock.Random.float(0, 100, 2, 2),
// createdAt: Mock.Random.datetime("yyyy-MM-dd HH:mm:ss"),
// updatedAt: Mock.Random.datetime("yyyy-MM-dd HH:mm:ss"),
// createdBy: Mock.Random.cname(),
// assignedTo: Mock.Random.cname(),
// totalDataCount: Mock.Random.integer(100, 10000),
// annotatedCount: Mock.Random.integer(10, 500),
// configuration: {
// labels: Mock.Random.shuffle([
// "正面",
// "负面",
// "中性",
// "人物",
// "地点",
// "组织",
// "时间",
// ]).slice(0, Mock.Random.integer(3, 5)),
// guidelines: Mock.Random.csentence(10, 50),
// qualityThreshold: Mock.Random.float(0.8, 1.0, 2, 2),
// },
// statistics: {
// accuracy: Mock.Random.float(0.85, 0.99, 2, 2),
// averageTime: Mock.Random.integer(30, 300), // seconds
// reviewCount: Mock.Random.integer(0, 50),
// },
};
}
const annotationTaskList = new Array(25).fill(null).map(annotationTaskItem);
// 标注数据项
function annotationDataItem() {
return {
id: Mock.Random.guid().replace(/[^a-zA-Z0-9]/g, ""),
taskId: Mock.Random.pick(annotationTaskList).id,
content: Mock.Random.cparagraph(1, 3),
originalData: {
text: Mock.Random.cparagraph(1, 3),
source: Mock.Random.url(),
metadata: {
author: Mock.Random.cname(),
timestamp: Mock.Random.datetime("yyyy-MM-dd HH:mm:ss"),
},
},
annotations: [
{
id: Mock.Random.guid().replace(/[^a-zA-Z0-9]/g, ""),
label: Mock.Random.pick(["正面", "负面", "中性"]),
confidence: Mock.Random.float(0.7, 1.0, 2, 2),
annotator: Mock.Random.cname(),
createdAt: Mock.Random.datetime("yyyy-MM-dd HH:mm:ss"),
isPreAnnotation: Mock.Random.boolean(),
},
],
status: Mock.Random.pick(["PENDING", "ANNOTATED", "REVIEWED", "REJECTED"]),
priority: Mock.Random.integer(1, 5),
createdAt: Mock.Random.datetime("yyyy-MM-dd HH:mm:ss"),
updatedAt: Mock.Random.datetime("yyyy-MM-dd HH:mm:ss"),
};
}
const annotationDataList = new Array(200).fill(null).map(annotationDataItem);
// 标注模板数据
function annotationTemplateItem() {
return {
id: Mock.Random.guid().replace(/[^a-zA-Z0-9]/g, ""),
name: Mock.Random.ctitle(5, 15),
description: Mock.Random.csentence(5, 25),
type: Mock.Random.pick([
"TEXT_CLASSIFICATION",
"NAMED_ENTITY_RECOGNITION",
"OBJECT_DETECTION",
"SEMANTIC_SEGMENTATION",
]),
category: Mock.Random.ctitle(3, 8),
labels: Mock.Random.shuffle([
"正面",
"负面",
"中性",
"人物",
"地点",
"组织",
"时间",
"产品",
"服务",
]).slice(0, Mock.Random.integer(3, 6)),
guidelines: Mock.Random.csentence(10, 50),
usageCount: Mock.Random.integer(0, 100),
createdAt: Mock.Random.datetime("yyyy-MM-dd HH:mm:ss"),
createdBy: Mock.Random.cname(),
};
}
const annotationTemplateList = new Array(15)
.fill(null)
.map(annotationTemplateItem);
// 标注者数据
function annotatorItem() {
return {
id: Mock.Random.guid().replace(/[^a-zA-Z0-9]/g, ""),
name: Mock.Random.cname(),
email: Mock.Random.email(),
role: Mock.Random.pick(["ANNOTATOR", "REVIEWER", "ADMIN"]),
skillLevel: Mock.Random.pick(["BEGINNER", "INTERMEDIATE", "EXPERT"]),
specialties: Mock.Random.shuffle([
"文本分类",
"命名实体识别",
"目标检测",
"语义分割",
]).slice(0, Mock.Random.integer(1, 3)),
statistics: {
totalAnnotations: Mock.Random.integer(100, 5000),
accuracy: Mock.Random.float(0.85, 0.99, 2, 2),
averageSpeed: Mock.Random.integer(50, 200), // annotations per hour
totalWorkTime: Mock.Random.integer(10, 500), // hours
},
status: Mock.Random.pick(["ACTIVE", "INACTIVE", "SUSPENDED"]),
createdAt: Mock.Random.datetime("yyyy-MM-dd HH:mm:ss"),
};
}
const annotatorList = new Array(20).fill(null).map(annotatorItem);
module.exports = function (router) {
// 获取标注任务列表
router.get(API.queryAnnotationTasksUsingGet, (req, res) => {
const { page = 0, size = 20, status, type } = req.query;
let filteredTasks = annotationTaskList;
if (status) {
filteredTasks = filteredTasks.filter((task) => task.status === status);
}
if (type) {
filteredTasks = filteredTasks.filter((task) => task.type === type);
}
const startIndex = page * size;
const endIndex = startIndex + parseInt(size);
const pageData = filteredTasks.slice(startIndex, endIndex);
res.send({
code: "0",
msg: "Success",
data: {
content: pageData,
totalElements: filteredTasks.length,
totalPages: Math.ceil(filteredTasks.length / size),
size: parseInt(size),
number: parseInt(page),
first: page == 0,
last: page >= Math.ceil(filteredTasks.length / size) - 1,
},
});
});
// 创建标注任务
router.post(API.createAnnotationTaskUsingPost, (req, res) => {
const newTask = {
...annotationTaskItem(),
...req.body,
id: Mock.Random.guid().replace(/[^a-zA-Z0-9]/g, ""),
status: "PENDING",
progress: 0,
annotatedCount: 0,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
annotationTaskList.push(newTask);
res.status(201).send({
code: "0",
msg: "Annotation task created successfully",
data: newTask,
});
});
// 获取标注任务详情
router.get(API.queryAnnotationTaskByIdUsingGet, (req, res) => {
const { taskId } = req.params;
const task = annotationTaskList.find((t) => t.id === taskId);
if (task) {
res.send({
code: "0",
msg: "Success",
data: task,
});
} else {
res.status(404).send({
code: "1",
msg: "Annotation task not found",
data: null,
});
}
});
// 更新标注任务
router.put(API.syncAnnotationTaskByIdUsingPost, (req, res) => {
const { taskId } = req.params;
const index = annotationTaskList.findIndex((t) => t.id === taskId);
if (index !== -1) {
annotationTaskList[index] = {
...annotationTaskList[index],
...req.body,
updatedAt: new Date().toISOString(),
};
res.send({
code: "0",
msg: "Annotation task updated successfully",
data: annotationTaskList[index],
});
} else {
res.status(404).send({
code: "1",
msg: "Annotation task not found",
data: null,
});
}
});
// 删除标注任务
router.delete(API.deleteAnnotationTaskByIdUsingDelete, (req, res) => {
const { taskId } = req.params;
const index = annotationTaskList.findIndex((t) => t.id === taskId);
if (index !== -1) {
annotationTaskList.splice(index, 1);
res.send({
code: "0",
msg: "Annotation task deleted successfully",
data: null,
});
} else {
res.status(404).send({
code: "1",
msg: "Annotation task not found",
data: null,
});
}
});
// 获取标注数据列表
router.get(API.queryAnnotationDataUsingGet, (req, res) => {
const { taskId } = req.params;
const { page = 0, size = 20, status } = req.query;
let filteredData = annotationDataList.filter(
(data) => data.taskId === taskId
);
if (status) {
filteredData = filteredData.filter((data) => data.status === status);
}
const startIndex = page * size;
const endIndex = startIndex + parseInt(size);
const pageData = filteredData.slice(startIndex, endIndex);
res.send({
code: "0",
msg: "Success",
data: {
content: pageData,
totalElements: filteredData.length,
totalPages: Math.ceil(filteredData.length / size),
size: parseInt(size),
number: parseInt(page),
},
});
});
// 提交标注
router.post(API.submitAnnotationUsingPost, (req, res) => {
const { taskId } = req.params;
const newAnnotation = {
id: Mock.Random.guid().replace(/[^a-zA-Z0-9]/g, ""),
taskId,
...req.body,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
res.status(201).send({
code: "0",
msg: "Annotation submitted successfully",
data: newAnnotation,
});
});
// 更新标注
router.put(API.updateAnnotationUsingPut, (req, res) => {
const { taskId, annotationId } = req.params;
res.send({
code: "0",
msg: "Annotation updated successfully",
data: {
id: annotationId,
taskId,
...req.body,
updatedAt: new Date().toISOString(),
},
});
});
// 删除标注
router.delete(API.deleteAnnotationUsingDelete, (req, res) => {
const { taskId, annotationId } = req.params;
res.send({
code: "0",
msg: "Annotation deleted successfully",
data: null,
});
});
// 开始标注任务
router.post(API.startAnnotationTaskUsingPost, (req, res) => {
const { taskId } = req.params;
const task = annotationTaskList.find((t) => t.id === taskId);
if (task) {
task.status = "IN_PROGRESS";
task.updatedAt = new Date().toISOString();
res.send({
code: "0",
msg: "Annotation task started successfully",
data: task,
});
} else {
res.status(404).send({
code: "1",
msg: "Annotation task not found",
data: null,
});
}
});
// 暂停标注任务
router.post(API.pauseAnnotationTaskUsingPost, (req, res) => {
const { taskId } = req.params;
const task = annotationTaskList.find((t) => t.id === taskId);
if (task) {
task.status = "PAUSED";
task.updatedAt = new Date().toISOString();
res.send({
code: "0",
msg: "Annotation task paused successfully",
data: task,
});
} else {
res.status(404).send({
code: "1",
msg: "Annotation task not found",
data: null,
});
}
});
// 恢复标注任务
router.post(API.resumeAnnotationTaskUsingPost, (req, res) => {
const { taskId } = req.params;
const task = annotationTaskList.find((t) => t.id === taskId);
if (task) {
task.status = "IN_PROGRESS";
task.updatedAt = new Date().toISOString();
res.send({
code: "0",
msg: "Annotation task resumed successfully",
data: task,
});
} else {
res.status(404).send({
code: "1",
msg: "Annotation task not found",
data: null,
});
}
});
// 完成标注任务
router.post(API.completeAnnotationTaskUsingPost, (req, res) => {
const { taskId } = req.params;
const task = annotationTaskList.find((t) => t.id === taskId);
if (task) {
task.status = "COMPLETED";
task.progress = 100;
task.updatedAt = new Date().toISOString();
res.send({
code: "0",
msg: "Annotation task completed successfully",
data: task,
});
} else {
res.status(404).send({
code: "1",
msg: "Annotation task not found",
data: null,
});
}
});
// 获取标注任务统计信息
router.get(API.getAnnotationTaskStatisticsUsingGet, (req, res) => {
const { taskId } = req.params;
const task = annotationTaskList.find((t) => t.id === taskId);
if (task) {
const statistics = {
taskId,
totalDataCount: task.totalDataCount,
annotatedCount: task.annotatedCount,
progress: task.progress,
accuracy: task.statistics.accuracy,
averageAnnotationTime: task.statistics.averageTime,
reviewCount: task.statistics.reviewCount,
qualityScore: Mock.Random.float(0.8, 0.99, 2, 2),
annotatorDistribution: {
[Mock.Random.cname()]: Mock.Random.integer(10, 100),
[Mock.Random.cname()]: Mock.Random.integer(10, 100),
[Mock.Random.cname()]: Mock.Random.integer(10, 100),
},
};
res.send({
code: "0",
msg: "Success",
data: statistics,
});
} else {
res.status(404).send({
code: "1",
msg: "Annotation task not found",
data: null,
});
}
});
// 获取整体标注统计信息
router.get(API.getAnnotationStatisticsUsingGet, (req, res) => {
const statistics = {
totalTasks: annotationTaskList.length,
completedTasks: annotationTaskList.filter((t) => t.status === "COMPLETED")
.length,
inProgressTasks: annotationTaskList.filter(
(t) => t.status === "IN_PROGRESS"
).length,
pendingTasks: annotationTaskList.filter((t) => t.status === "PENDING")
.length,
totalAnnotations: annotationDataList.length,
totalAnnotators: annotatorList.length,
averageAccuracy: Mock.Random.float(0.85, 0.95, 2, 2),
taskTypeDistribution: {
TEXT_CLASSIFICATION: Mock.Random.integer(5, 15),
NAMED_ENTITY_RECOGNITION: Mock.Random.integer(3, 10),
OBJECT_DETECTION: Mock.Random.integer(2, 8),
SEMANTIC_SEGMENTATION: Mock.Random.integer(1, 5),
},
};
res.send({
code: "0",
msg: "Success",
data: statistics,
});
});
// 获取标注模板列表
router.get(API.queryAnnotationTemplatesUsingGet, (req, res) => {
const { page = 0, size = 20, type } = req.query;
let filteredTemplates = annotationTemplateList;
if (type) {
filteredTemplates = filteredTemplates.filter(
(template) => template.type === type
);
}
const startIndex = page * size;
const endIndex = startIndex + parseInt(size);
const pageData = filteredTemplates.slice(startIndex, endIndex);
res.send({
code: "0",
msg: "Success",
data: {
content: pageData,
totalElements: filteredTemplates.length,
totalPages: Math.ceil(filteredTemplates.length / size),
size: parseInt(size),
number: parseInt(page),
},
});
});
// 创建标注模板
router.post(API.createAnnotationTemplateUsingPost, (req, res) => {
const newTemplate = {
...annotationTemplateItem(),
...req.body,
id: Mock.Random.guid().replace(/[^a-zA-Z0-9]/g, ""),
usageCount: 0,
createdAt: new Date().toISOString(),
};
annotationTemplateList.push(newTemplate);
res.status(201).send({
code: "0",
msg: "Annotation template created successfully",
data: newTemplate,
});
});
// 获取标注模板详情
router.get(API.queryAnnotationTemplateByIdUsingGet, (req, res) => {
const { templateId } = req.params;
const template = annotationTemplateList.find((t) => t.id === templateId);
if (template) {
res.send({
code: "0",
msg: "Success",
data: template,
});
} else {
res.status(404).send({
code: "1",
msg: "Annotation template not found",
data: null,
});
}
});
// 获取标注者列表
router.get(API.queryAnnotatorsUsingGet, (req, res) => {
const { page = 0, size = 20, status, skillLevel } = req.query;
let filteredAnnotators = annotatorList;
if (status) {
filteredAnnotators = filteredAnnotators.filter(
(annotator) => annotator.status === status
);
}
if (skillLevel) {
filteredAnnotators = filteredAnnotators.filter(
(annotator) => annotator.skillLevel === skillLevel
);
}
const startIndex = page * size;
const endIndex = startIndex + parseInt(size);
const pageData = filteredAnnotators.slice(startIndex, endIndex);
res.send({
code: "0",
msg: "Success",
data: {
content: pageData,
totalElements: filteredAnnotators.length,
totalPages: Math.ceil(filteredAnnotators.length / size),
size: parseInt(size),
number: parseInt(page),
},
});
});
// 分配标注者
router.post(API.assignAnnotatorUsingPost, (req, res) => {
const { taskId } = req.params;
const { annotatorIds } = req.body;
res.send({
code: "0",
msg: "Annotators assigned successfully",
data: {
taskId,
assignedAnnotators: annotatorIds,
assignedAt: new Date().toISOString(),
},
});
});
};

View File

@@ -0,0 +1,544 @@
const Mock = require("mockjs");
const API = require("../mock-apis.cjs");
function operatorItem() {
return {
id: Mock.Random.guid().replace(/[^a-zA-Z0-9]/g, ""),
name: Mock.Random.ctitle(3, 10),
description: Mock.Random.csentence(5, 20),
version: "1.0.0",
inputs: Mock.Random.integer(1, 5),
outputs: Mock.Random.integer(1, 5),
settings: JSON.stringify({
fileLength: {
name: "文档字数",
description:
"过滤字数不在指定范围内的文档,如[10,10000000]。若输入为空,则不对字数上/下限做限制。",
type: "range",
defaultVal: [10, 10000000],
min: 0,
max: 10000000000000000,
step: 1,
},
host: { type: "input", name: "主机地址", defaultVal: "localhost" },
limit: {
type: "range",
name: "读取行数",
defaultVal: [1000, 2000],
min: 100,
max: 10000,
step: 100,
},
filepath: { type: "input", name: "文件路径", defaultVal: "/path" },
encoding: {
type: "select",
name: "编码",
defaultVal: "utf-8",
options: ["utf-8", "gbk", "ascii"],
},
radio: {
type: "radio",
name: "radio",
defaultVal: "utf-8",
options: ["utf-8", "gbk", "ascii"],
},
features: {
type: "checkbox",
name: "特征列",
defaultVal: ["feature1", "feature3"],
options: ["feature1", "feature2", "feature3"],
},
repeatPhraseRatio: {
name: "文档词重复率",
description: "某个词的统计数/文档总词数 > 设定值,该文档被去除。",
type: "slider",
defaultVal: 0.5,
min: 0,
max: 1,
step: 0.1,
},
hitStopwords: {
name: "去除停用词",
description: "统计重复词时,选择是否要去除停用词。",
type: "switch",
defaultVal: false,
required: true,
checkedLabel: "去除",
unCheckedLabel: "不去除",
},
}),
categories: [Mock.Random.pick([3, 4, 5, 6, 7, 8, 9])],
isStar: Mock.Random.boolean(),
createdAt: Mock.Random.datetime("yyyy-MM-dd HH:mm:ss"),
updatedAt: Mock.Random.datetime("yyyy-MM-dd HH:mm:ss"),
};
}
const operatorList = new Array(50).fill(null).map(operatorItem);
// 清洗任务数据
function cleaningTaskItem() {
return {
id: Mock.Random.guid().replace(/[^a-zA-Z0-9]/g, ""),
name: Mock.Random.ctitle(5, 20),
description: Mock.Random.csentence(5, 30),
status: Mock.Random.pick([
"PENDING",
"RUNNING",
"COMPLETED",
"FAILED",
"STOPPED",
]),
srcDatasetId: Mock.Random.guid().replace(/[^a-zA-Z0-9]/g, ""),
srcDatasetName: Mock.Random.ctitle(5, 15),
destDatasetId: Mock.Random.guid().replace(/[^a-zA-Z0-9]/g, ""),
destDatasetName: Mock.Random.ctitle(5, 15),
progress: Mock.Random.float(0, 100, 2, 2),
startedAt: Mock.Random.datetime("yyyy-MM-dd HH:mm:ss"),
endedAt: Mock.Random.datetime("yyyy-MM-dd HH:mm:ss"),
createdAt: Mock.Random.datetime("yyyy-MM-dd HH:mm:ss"),
updatedAt: Mock.Random.datetime("yyyy-MM-dd HH:mm:ss"),
instance: operatorList,
};
}
const cleaningTaskList = new Array(20).fill(null).map(cleaningTaskItem);
// 清洗模板数据
function cleaningTemplateItem() {
return {
id: Mock.Random.guid().replace(/[^a-zA-Z0-9]/g, ""),
name: Mock.Random.ctitle(5, 15),
description: Mock.Random.csentence(5, 25),
instance: operatorList.slice(
Mock.Random.integer(0, 5),
Mock.Random.integer(6, 50)
),
category: Mock.Random.ctitle(3, 8),
createdAt: Mock.Random.datetime("yyyy-MM-dd HH:mm:ss"),
updatedAt: Mock.Random.datetime("yyyy-MM-dd HH:mm:ss"),
};
}
const cleaningTemplateList = new Array(15).fill(null).map(cleaningTemplateItem);
const categoryTree = [
{
id: 1,
name: "modal",
count: 7,
categories: [
{ id: 3, name: "text", count: 3, type: null, parentId: null },
{ id: 4, name: "image", count: 0, type: null, parentId: null },
{ id: 5, name: "audio", count: 0, type: null, parentId: null },
{ id: 6, name: "video", count: 0, type: null, parentId: null },
{
id: 7,
name: "multimodal",
count: 0,
type: null,
parentId: null,
},
],
},
{
id: 2,
name: "language",
count: 3,
categories: [
{ id: 8, name: "python", count: 2, type: null, parentId: null },
{ id: 9, name: "java", count: 1, type: null, parentId: null },
],
},
];
module.exports = function (router) {
// 获取清洗任务列表
router.get(API.queryCleaningTasksUsingGet, (req, res) => {
const { page = 0, size = 10, status } = req.query;
let filteredTasks = cleaningTaskList;
if (status) {
filteredTasks = cleaningTaskList.filter((task) => task.status === status);
}
const startIndex = page * size;
const endIndex = startIndex + parseInt(size);
const pageData = filteredTasks.slice(startIndex, endIndex);
res.send({
code: "0",
msg: "Success",
data: {
content: pageData,
totalElements: filteredTasks.length,
totalPages: Math.ceil(filteredTasks.length / size),
size: parseInt(size),
number: parseInt(page),
},
});
});
// 创建清洗任务
router.post(API.createCleaningTaskUsingPost, (req, res) => {
const newTask = {
...cleaningTaskItem(),
...req.body,
id: Mock.Random.guid().replace(/[^a-zA-Z0-9]/g, ""),
status: "PENDING",
createdAt: new Date().toISOString(),
};
cleaningTaskList.push(newTask);
res.status(201).send({
code: "0",
msg: "Cleaning task created successfully",
data: newTask,
});
});
// 获取清洗任务详情
router.get(API.queryCleaningTaskByIdUsingGet, (req, res) => {
const { taskId } = req.params;
const task = cleaningTaskList.find((j) => j.id === taskId);
if (task) {
res.send({
code: "0",
msg: "Success",
data: task,
});
} else {
res.status(404).send({
code: "1",
msg: "Cleaning task not found",
data: null,
});
}
});
// 删除清洗任务
router.delete(API.deleteCleaningTaskByIdUsingDelete, (req, res) => {
const { taskId } = req.params;
const index = cleaningTaskList.findIndex((j) => j.id === taskId);
if (index !== -1) {
cleaningTaskList.splice(index, 1);
res.send({
code: "0",
msg: "Cleaning task deleted successfully",
data: null,
});
} else {
res.status(404).send({
code: "1",
msg: "Cleaning task not found",
data: null,
});
}
});
// 执行清洗任务
router.post(API.executeCleaningTaskUsingPost, (req, res) => {
const { taskId } = req.params;
const task = cleaningTaskList.find((j) => j.id === taskId);
if (task) {
task.status = "running";
task.startTime = new Date().toISOString();
res.send({
code: "0",
msg: "Cleaning task execution started",
data: {
executionId: Mock.Random.guid().replace(/[^a-zA-Z0-9]/g, ""),
status: "running",
message: "Task execution started successfully",
},
});
} else {
res.status(404).send({
code: "1",
msg: "Cleaning task not found",
data: null,
});
}
});
// 停止清洗任务
router.post(API.stopCleaningTaskUsingPost, (req, res) => {
const { taskId } = req.params;
const task = cleaningTaskList.find((j) => j.id === taskId);
if (task) {
task.status = "pending";
task.endTime = new Date().toISOString();
res.send({
code: "0",
msg: "Cleaning task stopped successfully",
data: null,
});
} else {
res.status(404).send({
code: "1",
msg: "Cleaning task not found",
data: null,
});
}
});
// 获取清洗模板列表
router.get(API.queryCleaningTemplatesUsingGet, (req, res) => {
const { page = 0, size = 20 } = req.query;
const startIndex = page * size;
const endIndex = startIndex + parseInt(size);
const pageData = cleaningTemplateList.slice(startIndex, endIndex);
res.send({
code: "0",
msg: "Success",
data: { content: pageData, totalElements: cleaningTemplateList.length },
});
});
// 创建清洗模板
router.post(API.createCleaningTemplateUsingPost, (req, res) => {
const newTemplate = {
...cleaningTemplateItem(),
...req.body,
id: Mock.Random.guid().replace(/[^a-zA-Z0-9]/g, ""),
createdAt: new Date().toISOString(),
};
cleaningTemplateList.push(newTemplate);
res.status(201).send({
code: "0",
msg: "Cleaning template created successfully",
data: newTemplate,
});
});
// 获取清洗模板详情
router.get(API.queryCleaningTemplateByIdUsingGet, (req, res) => {
const { templateId } = req.params;
const template = cleaningTemplateList.find((t) => t.id === templateId);
if (template) {
res.send({
code: "0",
msg: "Success",
data: template,
});
} else {
res.status(404).send({
code: "1",
msg: "Cleaning template not found",
data: null,
});
}
});
// 删除清洗模板
router.delete(API.deleteCleaningTemplateByIdUsingDelete, (req, res) => {
const { templateId } = req.params;
const index = cleaningTemplateList.findIndex((t) => t.id === templateId);
if (index !== -1) {
cleaningTemplateList.splice(index, 1);
res.send({
code: "0",
msg: "Cleaning template deleted successfully",
data: null,
});
} else {
res.status(404).send({
code: "1",
msg: "Cleaning template not found",
data: null,
});
}
});
// 获取算子列表
router.post(API.queryOperatorsUsingPost, (req, res) => {
const {
page = 0,
size = 20,
categories = [],
operatorName = "",
labelName = "",
isStar,
} = req.body;
let filteredOperators = operatorList;
// 按分类筛选
if (categories && categories.length > 0) {
filteredOperators = filteredOperators.filter((op) =>
categories.includes(op.category.id)
);
}
// 按名称搜索
if (operatorName) {
filteredOperators = filteredOperators.filter((op) =>
op.name.toLowerCase().includes(operatorName.toLowerCase())
);
}
// 按标签筛选
if (labelName) {
filteredOperators = filteredOperators.filter((op) =>
op.labels.some((label) => label.name.includes(labelName))
);
}
// 按收藏状态筛选
if (typeof isStar === "boolean") {
filteredOperators = filteredOperators.filter(
(op) => op.isStar === isStar
);
}
const startIndex = page * size;
const endIndex = startIndex + parseInt(size);
const pageData = filteredOperators.slice(startIndex, endIndex);
res.send({
code: "0",
msg: "Success",
data: {
content: pageData,
totalElements: filteredOperators.length,
totalPages: Math.ceil(filteredOperators.length / size),
size: parseInt(size),
number: parseInt(page),
first: page === 0,
last: page >= Math.ceil(filteredOperators.length / size) - 1,
},
});
});
// 获取算子详情
router.get(API.queryOperatorByIdUsingGet, (req, res) => {
const { id } = req.params;
const operator = operatorList.find((op) => op.id === id);
if (operator) {
// 增加浏览次数模拟
operator.viewCount = (operator.viewCount || 0) + 1;
res.send({
code: "0",
msg: "Success",
data: operator,
});
} else {
res.status(404).send({
error: "OPERATOR_NOT_FOUND",
message: "算子不存在",
timestamp: new Date().toISOString(),
});
}
});
// 更新算子信息
router.put(API.updateOperatorByIdUsingPut, (req, res) => {
const { id } = req.params;
const index = operatorList.findIndex((op) => op.id === id);
if (index !== -1) {
operatorList[index] = {
...operatorList[index],
...req.body,
updatedAt: new Date().toISOString(),
};
res.send({
code: "0",
msg: "Operator updated successfully",
data: operatorList[index],
});
} else {
res.status(404).send({
error: "OPERATOR_NOT_FOUND",
message: "算子不存在",
timestamp: new Date().toISOString(),
});
}
});
// 创建算子
router.post(API.createOperatorUsingPost, (req, res) => {
const { name, description, version, category, documentation } = req.body;
const newOperator = {
...operatorItem(),
...req.body,
id: Mock.Random.guid().replace(/[^a-zA-Z0-9]/g, ""),
name,
description,
version,
category:
typeof category === "string"
? { id: category, name: category }
: category,
documentation,
status: "REVIEWING",
downloadCount: 0,
rating: 0,
ratingCount: 0,
isStar: false,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
operatorList.push(newOperator);
res.status(201).send({
code: "0",
msg: "Operator created successfully",
data: newOperator,
});
});
// 上传算子
router.post(API.uploadOperatorUsingPost, (req, res) => {
const { description } = req.body;
const newOperator = {
...operatorItem(),
description: description || "通过文件上传创建的算子",
status: "REVIEWING",
downloadCount: 0,
rating: 0,
ratingCount: 0,
isStar: false,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
operatorList.push(newOperator);
res.status(201).send({
code: "0",
msg: "Operator uploaded successfully",
data: newOperator,
});
});
// 获取算子分类树
router.get(API.queryCategoryTreeUsingGet, (req, res) => {
res.send({
code: "0",
msg: "Success",
data: {
page: 0,
size: categoryTree.length,
totalElements: categoryTree.length,
totalPages: 1,
content: categoryTree,
},
});
});
};

View File

@@ -0,0 +1,231 @@
const Mock = require("mockjs");
const API = require("../mock-apis.cjs");
const { Random } = Mock;
// 生成模拟数据归集统计信息
const collectionStatistics = {
period: Random.pick(["HOUR", "DAY", "WEEK", "MONTH"]),
totalTasks: Random.integer(50, 200),
activeTasks: Random.integer(10, 50),
successfulExecutions: Random.integer(30, 150),
failedExecutions: Random.integer(0, 50),
totalExecutions: Random.integer(20, 100),
avgExecutionTime: Random.integer(1000, 10000), // in milliseconds
avgThroughput: Random.integer(100, 1000), // records per second
topDataSources: new Array(5).fill(null).map(() => ({
dataSourceId: Mock.Random.guid().replace(/[^a-zA-Z0-9]/g, ""),
dataSourceName: Mock.Random.word(5, 15),
type: Mock.Random.pick([
"MySQL",
"PostgreSQL",
"ORACLE",
"SQLSERVER",
"MONGODB",
"REDIS",
"ELASTICSEARCH",
"HIVE",
"HDFS",
"KAFKA",
"HTTP",
"FILE",
]),
taskCount: Mock.Random.integer(1, 20),
executionCount: Mock.Random.integer(1, 50),
recordsProcessed: Mock.Random.integer(70, 100), // percentage
})),
};
// 生成模拟任务数据
function taskItem() {
return {
id: Mock.Random.guid().replace(/[^a-zA-Z0-9]/g, ""),
name: Mock.Random.ctitle(5, 20),
description: Mock.Random.csentence(5, 20),
syncMode: Mock.Random.pick(["ONCE", "SCHEDULED"]),
config: {
query: "SELECT * FROM table WHERE condition",
batchSize: Mock.Random.integer(100, 1000),
frequency: Mock.Random.integer(1, 60), // in minutes
},
scheduleExpression: "0 0 * * *", // cron expression
lastExecutionId: Mock.Random.guid().replace(/[^a-zA-Z0-9]/g, ""),
status: Mock.Random.pick([
"DRAFT",
"READY",
"RUNNING",
"FAILED",
"STOPPED",
"SUCCESS",
]),
createdAt: Mock.Random.datetime("yyyy-MM-dd HH:mm:ss"),
updatedAt: Mock.Random.datetime("yyyy-MM-dd HH:mm:ss"),
sourceDataSourceId: Mock.Random.guid().replace(/[^a-zA-Z0-9]/g, ""),
sourceDataSourceName: Mock.Random.ctitle(5, 20),
targetDataSourceId: Mock.Random.guid().replace(/[^a-zA-Z0-9]/g, ""),
targetDataSourceName: Mock.Random.ctitle(5, 20),
};
}
const taskList = new Array(50).fill(null).map(taskItem);
// 生成模拟任务执行日志数据
function executionLogItem() {
return {
id: Mock.Random.guid().replace(/[^a-zA-Z0-9]/g, ""),
taskName: Mock.Random.ctitle(5, 20),
dataSource: Mock.Random.ctitle(5, 15),
startTime: Mock.Random.datetime("yyyy-MM-dd HH:mm:ss"),
endTime: Mock.Random.datetime("yyyy-MM-dd HH:mm:ss"),
status: Mock.Random.pick(["SUCCESS", "FAILED", "RUNNING"]),
triggerType: Mock.Random.pick(["MANUAL", "SCHEDULED", "API"]),
duration: Mock.Random.integer(1, 120),
retryCount: Mock.Random.integer(0, 5),
recordsProcessed: Mock.Random.integer(100, 10000),
processId: Mock.Random.integer(1000, 9999),
errorMessage: Mock.Random.boolean() ? "" : Mock.Random.csentence(5, 20),
};
}
const executionLogList = new Array(100).fill(null).map(executionLogItem);
module.exports = function (router) {
// 获取数据统计信息
router.get(API.queryCollectionStatisticsUsingGet, (req, res) => {
res.send({
code: "0",
msg: "Success",
data: collectionStatistics,
});
});
// 获取任务列表
router.post(API.queryTasksUsingPost, (req, res) => {
const { searchTerm, filters, page = 1, size = 10 } = req.body;
let filteredTasks = taskList;
if (searchTerm) {
filteredTasks = filteredTasks.filter((task) =>
task.name.includes(searchTerm)
);
}
if (filters && filters.status && filters.status.length > 0) {
filteredTasks = filteredTasks.filter((task) =>
filters.status.includes(task.status)
);
}
const startIndex = (page - 1) * size;
const endIndex = startIndex + size;
const paginatedTasks = filteredTasks.slice(startIndex, endIndex);
res.send({
code: "0",
msg: "Success",
data: {
totalElements: filteredTasks.length,
page,
size,
results: paginatedTasks,
},
});
});
// 创建任务
router.post(API.createTaskUsingPost, (req, res) => {
taskList.unshift(taskItem()); // 添加一个新的任务到列表开头
res.send({
code: "0",
msg: "任务创建成功",
data: {
id: Mock.Random.guid().replace(/[^a-zA-Z0-9]/g, ""),
},
});
});
// 更新任务
router.post(API.updateTaskByIdUsingPut, (req, res) => {
const { id } = req.body;
res.send({
code: "0",
msg: "Data source task updated successfully",
data: taskList.find((task) => task.id === id),
});
});
// 删除任务
router.post(API.deleteTaskByIdUsingDelete, (req, res) => {
const { id } = req.body;
const index = taskList.findIndex((task) => task.id === id);
if (index !== -1) {
taskList.splice(index, 1);
}
res.send({
code: "0",
msg: "Data source task deleted successfully",
data: null,
});
});
// 执行任务
router.post(API.executeTaskByIdUsingPost, (req, res) => {
console.log("Received request to execute task", req.body);
const { id } = req.body;
console.log("Executing task with ID:", id);
taskList.find((task) => task.id === id).status = "RUNNING";
res.send({
code: "0",
msg: "Data source task execution started",
data: null,
});
});
// 停止任务
router.post(API.stopTaskByIdUsingPost, (req, res) => {
const { id } = req.body;
const task = taskList.find((task) => task.id === id);
if (task) {
task.status = "STOPPED";
}
res.send({
code: "0",
msg: "Data source task stopped successfully",
data: null,
});
});
// 获取任务执行日志
router.post(API.queryExecutionLogUsingPost, (req, res) => {
const { keyword, page = 1, size = 10, status } = req.body;
let filteredLogs = executionLogList;
if (keyword) {
filteredLogs = filteredLogs.filter((log) =>
log.taskName.includes(keyword)
);
}
if (status && status.length > 0) {
filteredLogs = filteredLogs.filter((log) => status.includes(log.status));
}
const startIndex = (page - 1) * size;
const endIndex = startIndex + size;
const paginatedLogs = filteredLogs.slice(startIndex, endIndex);
res.send({
code: "0",
msg: "Success",
data: {
totalElements: filteredLogs.length,
page,
size,
results: paginatedLogs,
},
});
});
// 获取任务执行日志详情
router.post(API.queryExecutionLogByIdUsingGet, (req, res) => {
const { id } = req.body;
const log = executionLogList.find((log) => log.id === id);
res.send({
code: "0",
msg: "Success",
data: log,
});
});
};

View File

@@ -0,0 +1,501 @@
const Mock = require("mockjs");
const API = require("../mock-apis.cjs");
// 质量指标枚举
const QualityMetrics = [
"COMPLETENESS",
"ACCURACY",
"CONSISTENCY",
"VALIDITY",
"UNIQUENESS",
"TIMELINESS"
];
// 适配性标准枚举
const CompatibilityCriteria = [
"FORMAT_COMPATIBILITY",
"SCHEMA_COMPATIBILITY",
"SIZE_ADEQUACY",
"DISTRIBUTION_MATCH",
"FEATURE_COVERAGE"
];
// 价值标准枚举
const ValueCriteria = [
"RARITY",
"DEMAND",
"QUALITY",
"COMPLETENESS",
"TIMELINESS",
"STRATEGIC_IMPORTANCE"
];
// 评估类型枚举
const EvaluationTypes = ["QUALITY", "COMPATIBILITY", "VALUE", "COMPREHENSIVE"];
// 评估状态枚举
const EvaluationStatuses = ["PENDING", "RUNNING", "COMPLETED", "FAILED"];
// 生成质量评估结果
function qualityEvaluationItem() {
return {
id: Mock.Random.guid().replace(/[^a-zA-Z0-9]/g, ""),
datasetId: Mock.Random.guid().replace(/[^a-zA-Z0-9]/g, ""),
status: Mock.Random.pick(EvaluationStatuses),
overallScore: Mock.Random.float(0.6, 1.0, 2, 2),
metrics: Mock.Random.shuffle(QualityMetrics).slice(0, Mock.Random.integer(3, 5)).map(metric => ({
metric,
score: Mock.Random.float(0.5, 1.0, 2, 2),
details: {
totalRecords: Mock.Random.integer(1000, 100000),
validRecords: Mock.Random.integer(800, 95000),
issues: Mock.Random.integer(0, 50)
},
issues: new Array(Mock.Random.integer(0, 3)).fill(null).map(() => ({
type: Mock.Random.pick(["MISSING_VALUE", "INVALID_FORMAT", "DUPLICATE", "OUTLIER"]),
severity: Mock.Random.pick(["LOW", "MEDIUM", "HIGH", "CRITICAL"]),
description: Mock.Random.csentence(5, 15),
affectedRecords: Mock.Random.integer(1, 1000),
suggestions: [Mock.Random.csentence(5, 20)]
}))
})),
recommendations: new Array(Mock.Random.integer(2, 5)).fill(null).map(() =>
Mock.Random.csentence(10, 30)
),
createdAt: Mock.Random.datetime("yyyy-MM-dd HH:mm:ss"),
detailedResults: {
fieldAnalysis: new Array(Mock.Random.integer(3, 8)).fill(null).map(() => ({
fieldName: Mock.Random.word(5, 10),
dataType: Mock.Random.pick(["STRING", "INTEGER", "FLOAT", "BOOLEAN", "DATE"]),
nullCount: Mock.Random.integer(0, 100),
uniqueCount: Mock.Random.integer(100, 1000),
statistics: {
mean: Mock.Random.float(0, 100, 2, 2),
median: Mock.Random.float(0, 100, 2, 2),
stdDev: Mock.Random.float(0, 50, 2, 2)
}
})),
distributionAnalysis: {
distributions: new Array(3).fill(null).map(() => ({
field: Mock.Random.word(5, 10),
type: Mock.Random.pick(["NORMAL", "UNIFORM", "SKEWED"]),
parameters: {}
})),
outliers: new Array(Mock.Random.integer(0, 5)).fill(null).map(() => ({
field: Mock.Random.word(5, 10),
value: Mock.Random.float(-100, 100, 2, 2),
zScore: Mock.Random.float(-3, 3, 2, 2)
})),
patterns: [
"数据分布较为均匀",
"存在少量异常值",
"部分字段相关性较强"
]
},
correlationAnalysis: {
correlationMatrix: new Array(5).fill(null).map(() =>
new Array(5).fill(null).map(() => Mock.Random.float(-1, 1, 2, 2))
),
significantCorrelations: new Array(Mock.Random.integer(1, 3)).fill(null).map(() => ({
field1: Mock.Random.word(5, 10),
field2: Mock.Random.word(5, 10),
correlation: Mock.Random.float(0.5, 1, 2, 2),
pValue: Mock.Random.float(0, 0.05, 3, 3)
}))
}
},
visualizations: new Array(Mock.Random.integer(2, 4)).fill(null).map(() => ({
type: Mock.Random.pick(["CHART", "GRAPH", "HISTOGRAM", "HEATMAP"]),
title: Mock.Random.ctitle(5, 15),
data: {
labels: new Array(5).fill(null).map(() => Mock.Random.word(3, 8)),
values: new Array(5).fill(null).map(() => Mock.Random.integer(0, 100))
},
config: {
width: 400,
height: 300,
color: Mock.Random.color()
}
}))
};
}
// 生成适配性评估结果
function compatibilityEvaluationItem() {
return {
id: Mock.Random.guid().replace(/[^a-zA-Z0-9]/g, ""),
datasetId: Mock.Random.guid().replace(/[^a-zA-Z0-9]/g, ""),
targetType: Mock.Random.pick(["LANGUAGE_MODEL", "CLASSIFICATION_MODEL", "RECOMMENDATION_SYSTEM", "CUSTOM_TASK"]),
compatibilityScore: Mock.Random.float(0.6, 1.0, 2, 2),
results: Mock.Random.shuffle(CompatibilityCriteria).slice(0, Mock.Random.integer(3, 5)).map(criterion => ({
criterion,
score: Mock.Random.float(0.5, 1.0, 2, 2),
status: Mock.Random.pick(["PASS", "WARN", "FAIL"]),
details: Mock.Random.csentence(10, 30)
})),
suggestions: new Array(Mock.Random.integer(2, 4)).fill(null).map(() =>
Mock.Random.csentence(10, 25)
),
createdAt: Mock.Random.datetime("yyyy-MM-dd HH:mm:ss")
};
}
// 生成价值评估结果
function valueEvaluationItem() {
return {
id: Mock.Random.guid().replace(/[^a-zA-Z0-9]/g, ""),
datasetId: Mock.Random.guid().replace(/[^a-zA-Z0-9]/g, ""),
valueScore: Mock.Random.float(0.6, 1.0, 2, 2),
monetaryValue: Mock.Random.float(10000, 1000000, 2, 2),
strategicValue: Mock.Random.float(0.6, 1.0, 2, 2),
results: Mock.Random.shuffle(ValueCriteria).slice(0, Mock.Random.integer(3, 5)).map(criterion => ({
criterion,
score: Mock.Random.float(0.5, 1.0, 2, 2),
impact: Mock.Random.pick(["LOW", "MEDIUM", "HIGH"]),
explanation: Mock.Random.csentence(10, 30)
})),
insights: new Array(Mock.Random.integer(3, 6)).fill(null).map(() =>
Mock.Random.csentence(15, 40)
),
createdAt: Mock.Random.datetime("yyyy-MM-dd HH:mm:ss")
};
}
// 生成评估报告
function evaluationReportItem() {
const type = Mock.Random.pick(EvaluationTypes);
return {
id: Mock.Random.guid().replace(/[^a-zA-Z0-9]/g, ""),
datasetId: Mock.Random.guid().replace(/[^a-zA-Z0-9]/g, ""),
datasetName: Mock.Random.ctitle(5, 15),
type,
status: Mock.Random.pick(EvaluationStatuses),
overallScore: Mock.Random.float(0.6, 1.0, 2, 2),
summary: Mock.Random.csentence(20, 50),
createdAt: Mock.Random.datetime("yyyy-MM-dd HH:mm:ss"),
completedAt: Mock.Random.datetime("yyyy-MM-dd HH:mm:ss"),
qualityResults: type === "QUALITY" || type === "COMPREHENSIVE" ? qualityEvaluationItem() : null,
compatibilityResults: type === "COMPATIBILITY" || type === "COMPREHENSIVE" ? compatibilityEvaluationItem() : null,
valueResults: type === "VALUE" || type === "COMPREHENSIVE" ? valueEvaluationItem() : null,
attachments: new Array(Mock.Random.integer(1, 3)).fill(null).map(() => ({
id: Mock.Random.guid().replace(/[^a-zA-Z0-9]/g, ""),
name: Mock.Random.word(5, 10) + "." + Mock.Random.pick(["pdf", "xlsx", "json"]),
type: Mock.Random.pick(["PDF", "EXCEL", "JSON"]),
size: Mock.Random.integer(1024, 1024 * 1024),
downloadUrl: "/api/v1/evaluation/attachments/" + Mock.Random.guid()
}))
};
}
const qualityEvaluationList = new Array(30).fill(null).map(qualityEvaluationItem);
const compatibilityEvaluationList = new Array(20).fill(null).map(compatibilityEvaluationItem);
const valueEvaluationList = new Array(25).fill(null).map(valueEvaluationItem);
const evaluationReportList = new Array(50).fill(null).map(evaluationReportItem);
module.exports = function (router) {
// 数据质量评估
router.post(API.evaluateDataQualityUsingPost, (req, res) => {
const { datasetId, metrics, sampleSize, parameters } = req.body;
const newEvaluation = {
...qualityEvaluationItem(),
datasetId,
status: "RUNNING",
metrics: metrics.map(metric => ({
metric,
score: Mock.Random.float(0.5, 1.0, 2, 2),
details: {
totalRecords: sampleSize || Mock.Random.integer(1000, 100000),
validRecords: Mock.Random.integer(800, 95000),
issues: Mock.Random.integer(0, 50)
},
issues: []
})),
createdAt: new Date().toISOString()
};
qualityEvaluationList.push(newEvaluation);
// 模拟异步处理,2秒后完成
setTimeout(() => {
newEvaluation.status = "COMPLETED";
}, 2000);
res.send({
code: "0",
msg: "Quality evaluation started successfully",
data: newEvaluation
});
});
// 获取质量评估结果
router.get(API.getQualityEvaluationByIdUsingGet, (req, res) => {
const { evaluationId } = req.params;
const evaluation = qualityEvaluationList.find(e => e.id === evaluationId);
if (evaluation) {
res.send({
code: "0",
msg: "Success",
data: evaluation
});
} else {
res.status(404).send({
code: "1",
msg: "Quality evaluation not found",
data: null
});
}
});
// 适配性评估
router.post(API.evaluateCompatibilityUsingPost, (req, res) => {
const { datasetId, targetType, targetConfig, evaluationCriteria } = req.body;
const newEvaluation = {
...compatibilityEvaluationItem(),
datasetId,
targetType,
results: evaluationCriteria.map(criterion => ({
criterion,
score: Mock.Random.float(0.5, 1.0, 2, 2),
status: Mock.Random.pick(["PASS", "WARN", "FAIL"]),
details: Mock.Random.csentence(10, 30)
})),
createdAt: new Date().toISOString()
};
compatibilityEvaluationList.push(newEvaluation);
res.send({
code: "0",
msg: "Compatibility evaluation completed successfully",
data: newEvaluation
});
});
// 价值评估
router.post(API.evaluateValueUsingPost, (req, res) => {
const { datasetId, valueCriteria, marketContext, businessContext } = req.body;
const newEvaluation = {
...valueEvaluationItem(),
datasetId,
results: valueCriteria.map(criterion => ({
criterion,
score: Mock.Random.float(0.5, 1.0, 2, 2),
impact: Mock.Random.pick(["LOW", "MEDIUM", "HIGH"]),
explanation: Mock.Random.csentence(10, 30)
})),
createdAt: new Date().toISOString()
};
valueEvaluationList.push(newEvaluation);
res.send({
code: "0",
msg: "Value evaluation completed successfully",
data: newEvaluation
});
});
// 获取评估报告列表
router.get(API.queryEvaluationReportsUsingGet, (req, res) => {
const { page = 0, size = 20, type, datasetId } = req.query;
let filteredReports = evaluationReportList;
if (type) {
filteredReports = filteredReports.filter(report => report.type === type);
}
if (datasetId) {
filteredReports = filteredReports.filter(report => report.datasetId === datasetId);
}
const startIndex = page * size;
const endIndex = startIndex + parseInt(size);
const pageData = filteredReports.slice(startIndex, endIndex);
res.send({
code: "0",
msg: "Success",
data: {
content: pageData,
totalElements: filteredReports.length,
totalPages: Math.ceil(filteredReports.length / size),
size: parseInt(size),
number: parseInt(page)
}
});
});
// 获取评估报告详情
router.get(API.getEvaluationReportByIdUsingGet, (req, res) => {
const { reportId } = req.params;
const report = evaluationReportList.find(r => r.id === reportId);
if (report) {
res.send({
code: "0",
msg: "Success",
data: report
});
} else {
res.status(404).send({
code: "1",
msg: "Evaluation report not found",
data: null
});
}
});
// 导出评估报告
router.get(API.exportEvaluationReportUsingGet, (req, res) => {
const { reportId } = req.params;
const { format = "PDF" } = req.query;
const report = evaluationReportList.find(r => r.id === reportId);
if (report) {
const fileName = `evaluation_report_${reportId}.${format.toLowerCase()}`;
res.setHeader('Content-Disposition', `attachment; filename="${fileName}"`);
res.setHeader('Content-Type', 'application/octet-stream');
res.send(`Mock ${format} content for evaluation report ${reportId}`);
} else {
res.status(404).send({
code: "1",
msg: "Evaluation report not found",
data: null
});
}
});
// 批量评估
router.post(API.batchEvaluationUsingPost, (req, res) => {
const { datasetIds, evaluationTypes, parameters } = req.body;
const batchId = Mock.Random.guid().replace(/[^a-zA-Z0-9]/g, "");
const totalTasks = datasetIds.length * evaluationTypes.length;
// 为每个数据集和评估类型组合创建任务
datasetIds.forEach(datasetId => {
evaluationTypes.forEach(type => {
const report = {
...evaluationReportItem(),
datasetId,
type,
status: "PENDING",
batchId
};
evaluationReportList.push(report);
// 模拟异步处理
setTimeout(() => {
report.status = "COMPLETED";
}, Mock.Random.integer(3000, 10000));
});
});
res.status(202).send({
code: "0",
msg: "Batch evaluation submitted successfully",
data: {
batchId,
status: "SUBMITTED",
totalTasks,
submittedAt: new Date().toISOString()
}
});
});
// 获取批量评估状态
router.get("/api/v1/evaluation/batch/:batchId", (req, res) => {
const { batchId } = req.params;
const batchReports = evaluationReportList.filter(r => r.batchId === batchId);
const completedTasks = batchReports.filter(r => r.status === "COMPLETED").length;
const runningTasks = batchReports.filter(r => r.status === "RUNNING").length;
const pendingTasks = batchReports.filter(r => r.status === "PENDING").length;
const failedTasks = batchReports.filter(r => r.status === "FAILED").length;
let overallStatus = "COMPLETED";
if (runningTasks > 0 || pendingTasks > 0) {
overallStatus = "RUNNING";
} else if (failedTasks > 0) {
overallStatus = "PARTIAL_FAILED";
}
res.send({
code: "0",
msg: "Success",
data: {
batchId,
status: overallStatus,
totalTasks: batchReports.length,
completedTasks,
runningTasks,
pendingTasks,
failedTasks,
progress: batchReports.length > 0 ? Math.round((completedTasks / batchReports.length) * 100) : 0,
reports: batchReports
}
});
});
// 获取评估统计信息
router.get("/api/v1/evaluation/statistics", (req, res) => {
const { timeRange = "LAST_30_DAYS" } = req.query;
const statistics = {
totalEvaluations: evaluationReportList.length,
completedEvaluations: evaluationReportList.filter(r => r.status === "COMPLETED").length,
runningEvaluations: evaluationReportList.filter(r => r.status === "RUNNING").length,
failedEvaluations: evaluationReportList.filter(r => r.status === "FAILED").length,
averageScore: Mock.Random.float(0.75, 0.95, 2, 2),
evaluationTypeDistribution: {
QUALITY: evaluationReportList.filter(r => r.type === "QUALITY").length,
COMPATIBILITY: evaluationReportList.filter(r => r.type === "COMPATIBILITY").length,
VALUE: evaluationReportList.filter(r => r.type === "VALUE").length,
COMPREHENSIVE: evaluationReportList.filter(r => r.type === "COMPREHENSIVE").length
},
scoreDistribution: {
excellent: evaluationReportList.filter(r => r.overallScore >= 0.9).length,
good: evaluationReportList.filter(r => r.overallScore >= 0.8 && r.overallScore < 0.9).length,
fair: evaluationReportList.filter(r => r.overallScore >= 0.6 && r.overallScore < 0.8).length,
poor: evaluationReportList.filter(r => r.overallScore < 0.6).length
},
trends: new Array(30).fill(null).map((_, index) => ({
date: Mock.Random.date("yyyy-MM-dd"),
evaluations: Mock.Random.integer(5, 50),
averageScore: Mock.Random.float(0.7, 0.95, 2, 2)
}))
};
res.send({
code: "0",
msg: "Success",
data: statistics
});
});
// 删除评估报告
router.delete("/api/v1/evaluation/reports/:reportId", (req, res) => {
const { reportId } = req.params;
const index = evaluationReportList.findIndex(r => r.id === reportId);
if (index !== -1) {
evaluationReportList.splice(index, 1);
res.send({
code: "0",
msg: "Evaluation report deleted successfully",
data: null
});
} else {
res.status(404).send({
code: "1",
msg: "Evaluation report not found",
data: null
});
}
});
};

View File

@@ -0,0 +1,437 @@
const Mock = require("mockjs");
const API = require("../mock-apis.cjs");
function tagItem() {
return {
id: Mock.Random.guid().replace(/[^a-zA-Z0-9]/g, ""),
name: Mock.Random.word(3, 10),
description: Mock.Random.csentence(5, 20),
color: Mock.Random.color(),
usageCount: Mock.Random.integer(0, 100),
};
}
const tagList = new Array(20).fill(null).map((_, index) => tagItem(index));
function datasetItem() {
return {
id: Mock.Random.guid().replace(/[^a-zA-Z0-9]/g, ""),
name: Mock.Random.ctitle(5, 20),
type: Mock.Random.pick(["TEXT", "IMAGE", "AUDIO", "VIDEO"]),
status: Mock.Random.pick(["ACTIVE", "INACTIVE", "PROCESSING"]),
tags: Mock.Random.shuffle(tagList).slice(0, Mock.Random.integer(1, 3)),
totalSize: Mock.Random.integer(1024, 1024 * 1024 * 1024), // in bytes
description: Mock.Random.cparagraph(1, 3),
createdAt: Mock.Random.datetime("yyyy-MM-dd HH:mm:ss"),
updatedAt: Mock.Random.datetime("yyyy-MM-dd HH:mm:ss"),
createdBy: Mock.Random.cname(),
updatedBy: Mock.Random.cname(),
};
}
const datasetList = new Array(50)
.fill(null)
.map((_, index) => datasetItem(index));
function datasetFileItem() {
return {
id: Mock.Random.guid().replace(/[^a-zA-Z0-9]/g, ""),
fileName:
Mock.Random.word(5, 15) +
"." +
Mock.Random.pick(["csv", "json", "xml", "parquet", "avro"]),
originName:
Mock.Random.word(5, 15) +
"." +
Mock.Random.pick(["csv", "json", "xml", "parquet", "avro"]),
fileType: Mock.Random.pick(["CSV", "JSON", "XML", "Parquet", "Avro"]),
size: Mock.Random.integer(1024, 1024 * 1024 * 1024), // in bytes
type: Mock.Random.pick(["CSV", "JSON", "XML", "Parquet", "Avro"]),
status: Mock.Random.pick(["UPLOADED", "PROCESSING", "COMPLETED", "ERROR"]),
description: Mock.Random.csentence(5, 20),
filePath: "/path/to/file/" + Mock.Random.word(5, 10),
uploadedAt: Mock.Random.datetime("yyyy-MM-dd HH:mm:ss"),
uploadedBy: Mock.Random.cname(),
};
}
const datasetFileList = new Array(200)
.fill(null)
.map((_, index) => datasetFileItem(index));
const datasetStatistics = {
count: {
text: 10,
image: 34,
audio: 23,
video: 5,
},
size: {
text: "120 MB",
image: "3.4 GB",
audio: "2.3 GB",
video: "15 GB",
},
totalDatasets: datasetList.length,
totalFiles: datasetFileList.length,
completedFiles: datasetFileList.filter((file) => file.status === "COMPLETED")
.length,
totalSize: datasetFileList.reduce((acc, file) => acc + file.size, 0), // in bytes
completionRate:
datasetFileList.length === 0
? 0
: Math.round(
(datasetFileList.filter((file) => file.status === "COMPLETED")
.length /
datasetFileList.length) *
100
), // percentage
};
const datasetTypes = [
{
code: "PRETRAIN",
name: "预训练数据集",
description: "用于模型预训练的大规模数据集",
supportedFormats: ["txt", "json", "csv", "parquet"],
icon: "brain",
},
{
code: "FINE_TUNE",
name: "微调数据集",
description: "用于模型微调的专业数据集",
supportedFormats: ["json", "csv", "xlsx"],
icon: "tune",
},
{
code: "EVAL",
name: "评估数据集",
description: "用于模型评估的标准数据集",
supportedFormats: ["json", "csv", "xml"],
icon: "assessment",
},
];
module.exports = { datasetList };
module.exports = function (router) {
// 获取数据统计信息
router.get(API.queryDatasetStatisticsUsingGet, (req, res) => {
res.send({
code: "0",
msg: "Success",
data: datasetStatistics,
});
});
// 创建数据
router.post(API.createDatasetUsingPost, (req, res) => {
const newDataset = {
...req.body,
id: Mock.Random.guid().replace(/[^a-zA-Z0-9]/g, ""),
status: "ACTIVE",
fileCount: 0,
totalSize: 0,
completionRate: 0,
createdAt: Mock.Random.datetime("yyyy-MM-dd HH:mm:ss"),
updatedAt: Mock.Random.datetime("yyyy-MM-dd HH:mm:ss"),
createdBy: "Admin",
updatedBy: "Admin",
tags: tagList.filter((tag) => req.body?.tagIds?.includes?.(tag.id)),
};
datasetList.unshift(newDataset); // Add to the beginning of the list
res.send({
code: "0",
msg: "Dataset created successfully",
data: newDataset,
});
});
// 获取数据集列表
router.get(API.queryDatasetsUsingGet, (req, res) => {
const { page = 0, size = 10, keyword, type, status, tags } = req.query;
console.log("Received query params:", req.query);
let filteredDatasets = datasetList;
if (keyword) {
console.log("filter keyword:", keyword);
filteredDatasets = filteredDatasets.filter(
(dataset) =>
dataset.name.includes(keyword) ||
dataset.description.includes(keyword)
);
}
if (type) {
console.log("filter type:", type);
filteredDatasets = filteredDatasets.filter(
(dataset) => dataset.type === type
);
}
if (status) {
console.log("filter status:", status);
filteredDatasets = filteredDatasets.filter(
(dataset) => dataset.status === status
);
}
if (tags && tags.length > 0) {
console.log("filter tags:", tags);
filteredDatasets = filteredDatasets.filter((dataset) =>
tags.every((tag) => dataset.tags.some((t) => t.name === tag))
);
}
const totalElements = filteredDatasets.length;
const paginatedDatasets = filteredDatasets.slice(
page * size,
(page + 1) * size
);
res.send({
code: "0",
msg: "Success",
data: {
totalElements,
page,
size,
content: paginatedDatasets,
},
});
});
// 根据ID获取数据集详情
router.get(API.queryDatasetByIdUsingGet, (req, res) => {
const { id } = req.params;
const dataset = datasetList.find((d) => d.id === id);
if (dataset) {
res.send({
code: "0",
msg: "Success",
data: dataset,
});
} else {
res.status(404).send({
code: "1",
msg: "Dataset not found",
data: null,
});
}
});
// 更新数据集
router.put(API.updateDatasetByIdUsingPut, (req, res) => {
const { id } = req.params;
let { tags } = req.body;
const index = datasetList.findIndex((d) => d.id === id);
tags = [...datasetList[index].tags.map((tag) => tag.name), ...tags];
if (index !== -1) {
datasetList[index] = {
...datasetList[index],
...req.body,
tags: tagList.filter((tag) => tags?.includes?.(tag.name)),
updatedAt: new Date().toISOString(),
updatedBy: "Admin",
};
res.send({
code: "0",
msg: "Dataset updated successfully",
data: datasetList[index],
});
} else {
res.status(404).send({
code: "1",
msg: "Dataset not found",
data: null,
});
}
});
// 删除数据集
router.delete(API.deleteDatasetByIdUsingDelete, (req, res) => {
const { datasetId } = req.params;
const index = datasetList.findIndex((d) => d.id === datasetId);
if (index !== -1) {
datasetList.splice(index, 1);
res.status(204).send();
} else {
res.status(404).send({
code: "1",
msg: "Dataset not found",
data: null,
});
}
});
// 获取数据集文件列表
router.get(API.queryFilesUsingGet, (req, res) => {
const { datasetId } = req.params;
const { page = 0, size = 20, fileType, status } = req.query;
let filteredFiles = datasetFileList;
if (fileType) {
filteredFiles = filteredFiles.filter(
(file) => file.fileType === fileType
);
}
if (status) {
filteredFiles = filteredFiles.filter((file) => file.status === status);
}
const startIndex = page * size;
const endIndex = startIndex + parseInt(size);
const pageData = filteredFiles.slice(startIndex, endIndex);
res.send({
code: "0",
msg: "Success",
data: {
content: pageData,
page: parseInt(page),
size: parseInt(size),
totalElements: filteredFiles.length,
},
});
});
// 上传文件到数据集
router.post(API.uploadFileUsingPost, (req, res) => {
const { datasetId } = req.params;
const newFile = {
...datasetFileItem(),
...req.body,
id: Mock.Random.guid().replace(/[^a-zA-Z0-9]/g, ""),
uploadedAt: new Date().toISOString(),
uploadedBy: "Admin",
};
datasetFileList.push(newFile);
res.status(201).send({
code: "0",
msg: "File uploaded successfully",
data: newFile,
});
});
// 获取文件详情
router.get(API.queryFileByIdUsingGet, (req, res) => {
const { datasetId, fileId } = req.params;
const file = datasetFileList.find((f) => f.id === fileId);
if (file) {
res.send({
code: "0",
msg: "Success",
data: file,
});
} else {
res.status(404).send({
code: "1",
msg: "File not found",
data: null,
});
}
});
// 删除文件
router.delete(API.deleteFileByIdUsingDelete, (req, res) => {
const { datasetId, fileId } = req.params;
const index = datasetFileList.findIndex((f) => f.id === fileId);
if (index !== -1) {
datasetFileList.splice(index, 1);
res.status(204).send();
} else {
res.status(404).send({
code: "1",
msg: "File not found",
data: null,
});
}
});
// 下载文件
router.get(API.downloadFileByIdUsingGet, (req, res) => {
const { datasetId, fileId } = req.params;
const file = datasetFileList.find((f) => f.id === fileId);
if (file) {
res.setHeader(
"Content-Disposition",
`attachment; filename="${file.fileName}"`
);
res.setHeader("Content-Type", "application/octet-stream");
res.send(`Mock file content for ${file.fileName}`);
} else {
res.status(404).send({
code: "1",
msg: "File not found",
data: null,
});
}
});
// 获取数据集类型列表
router.get(API.queryDatasetTypesUsingGet, (req, res) => {
res.send({
code: "0",
msg: "Success",
data: datasetTypes,
});
});
// 获取标签列表
router.get(API.queryTagsUsingGet, (req, res) => {
const { keyword } = req.query;
let filteredTags = tagList;
if (keyword) {
filteredTags = tagList.filter((tag) =>
tag.name.toLowerCase().includes(keyword.toLowerCase())
);
}
res.send({
code: "0",
msg: "Success",
data: filteredTags,
});
});
// 创建标签
router.post(API.createTagUsingPost, (req, res) => {
const newTag = {
...tagItem(),
...req.body,
id: Mock.Random.guid().replace(/[^a-zA-Z0-9]/g, ""),
usageCount: 0,
};
tagList.push(newTag);
res.status(201).send({
code: "0",
msg: "Tag created successfully",
data: newTag,
});
});
router.post(API.preUploadFileUsingPost, (req, res) => {
res.status(201).send(Mock.Random.guid());
});
// 上传
router.post(API.uploadFileChunkUsingPost, (req, res) => {
res.status(500).send({ message: "Simulated upload failure" });
// res.status(201).send({ data: "success" });
});
// 取消上传
router.put(API.cancelUploadUsingPut, (req, res) => {
res.status(201).send({ data: "success" });
});
};

View File

@@ -0,0 +1,522 @@
const Mock = require("mockjs");
const API = require("../mock-apis.cjs");
// 合成类型枚举
const SynthesisTypes = [
"INSTRUCTION_TUNING",
"COT_DISTILLATION",
"DIALOGUE_GENERATION",
"TEXT_AUGMENTATION",
"MULTIMODAL_SYNTHESIS",
"CUSTOM"
];
// 任务状态枚举
const JobStatuses = ["PENDING", "RUNNING", "COMPLETED", "FAILED", "CANCELLED"];
// 模型配置
function modelConfigItem() {
return {
modelName: Mock.Random.pick([
"gpt-3.5-turbo",
"gpt-4",
"claude-3",
"llama-2-70b",
"qwen-max"
]),
temperature: Mock.Random.float(0.1, 1.0, 2, 2),
maxTokens: Mock.Random.pick([512, 1024, 2048, 4096]),
topP: Mock.Random.float(0.1, 1.0, 2, 2),
frequencyPenalty: Mock.Random.float(0, 2.0, 2, 2)
};
}
// 合成模板数据
function synthesisTemplateItem() {
const type = Mock.Random.pick(SynthesisTypes);
return {
id: Mock.Random.guid().replace(/[^a-zA-Z0-9]/g, ""),
name: Mock.Random.ctitle(5, 20),
description: Mock.Random.csentence(5, 30),
type,
category: Mock.Random.pick([
"教育培训", "对话系统", "内容生成", "代码生成", "多模态", "自定义"
]),
modelConfig: modelConfigItem(),
enabled: Mock.Random.boolean(),
promptTemplate: type === "INSTRUCTION_TUNING"
? "请根据以下主题生成一个指令:{topic}\n指令应该包含:\n1. 明确的任务描述\n2. 具体的输入要求\n3. 期望的输出格式"
: type === "COT_DISTILLATION"
? "问题:{question}\n请提供详细的推理步骤,然后给出最终答案。\n推理过程:\n1. 分析问题的关键信息\n2. 应用相关知识和规则\n3. 逐步推导出结论"
: "请根据以下模板生成内容:{template}",
parameters: {
maxLength: Mock.Random.integer(100, 2000),
diversity: Mock.Random.float(0.1, 1.0, 2, 2),
quality: Mock.Random.float(0.7, 1.0, 2, 2)
},
examples: new Array(Mock.Random.integer(2, 5)).fill(null).map(() => ({
input: Mock.Random.csentence(10, 30),
output: Mock.Random.csentence(20, 50),
explanation: Mock.Random.csentence(5, 20)
})),
createdAt: Mock.Random.datetime("yyyy-MM-dd HH:mm:ss"),
updatedAt: Mock.Random.datetime("yyyy-MM-dd HH:mm:ss")
};
}
const synthesisTemplateList = new Array(25).fill(null).map(synthesisTemplateItem);
// 合成任务数据
function synthesisJobItem() {
const template = Mock.Random.pick(synthesisTemplateList);
const targetCount = Mock.Random.integer(100, 10000);
const generatedCount = Mock.Random.integer(0, targetCount);
const progress = targetCount > 0 ? (generatedCount / targetCount) * 100 : 0;
return {
id: Mock.Random.guid().replace(/[^a-zA-Z0-9]/g, ""),
name: Mock.Random.ctitle(5, 20),
description: Mock.Random.csentence(5, 30),
templateId: template.id,
template: {
id: template.id,
name: template.name,
type: template.type
},
status: Mock.Random.pick(JobStatuses),
progress: Math.round(progress * 100) / 100,
targetCount,
generatedCount,
startTime: Mock.Random.datetime("yyyy-MM-dd HH:mm:ss"),
endTime: progress >= 100 ? Mock.Random.datetime("yyyy-MM-dd HH:mm:ss") : null,
createdAt: Mock.Random.datetime("yyyy-MM-dd HH:mm:ss"),
updatedAt: Mock.Random.datetime("yyyy-MM-dd HH:mm:ss"),
statistics: {
totalGenerated: generatedCount,
successfulGenerated: Math.floor(generatedCount * Mock.Random.float(0.85, 0.98, 2, 2)),
failedGenerated: Math.floor(generatedCount * Mock.Random.float(0.02, 0.15, 2, 2)),
averageLength: Mock.Random.integer(50, 500),
uniqueCount: Math.floor(generatedCount * Mock.Random.float(0.8, 0.95, 2, 2))
},
samples: new Array(Math.min(10, generatedCount)).fill(null).map(() => ({
id: Mock.Random.guid().replace(/[^a-zA-Z0-9]/g, ""),
content: Mock.Random.cparagraph(1, 3),
score: Mock.Random.float(0.6, 1.0, 2, 2),
metadata: {
length: Mock.Random.integer(50, 500),
model: template.modelConfig.modelName,
timestamp: Mock.Random.datetime("yyyy-MM-dd HH:mm:ss")
},
createdAt: Mock.Random.datetime("yyyy-MM-dd HH:mm:ss")
}))
};
}
const synthesisJobList = new Array(30).fill(null).map(synthesisJobItem);
// 生成指令数据
function generatedInstructionItem() {
return {
instruction: Mock.Random.csentence(10, 30),
input: Mock.Random.csentence(5, 20),
output: Mock.Random.csentence(10, 40),
quality: Mock.Random.float(0.7, 1.0, 2, 2)
};
}
// COT 示例数据
function cotExampleItem() {
return {
question: Mock.Random.csentence(10, 25) + "?",
reasoning: Mock.Random.cparagraph(2, 4),
answer: Mock.Random.csentence(5, 15)
};
}
// 蒸馏COT数据
function distilledCOTDataItem() {
return {
question: Mock.Random.csentence(10, 25) + "?",
reasoning: Mock.Random.cparagraph(2, 4),
answer: Mock.Random.csentence(5, 15),
confidence: Mock.Random.float(0.7, 1.0, 2, 2)
};
}
module.exports = function (router) {
// 获取合成模板列表
router.get(API.querySynthesisTemplatesUsingGet, (req, res) => {
const { page = 0, size = 20, type } = req.query;
let filteredTemplates = synthesisTemplateList;
if (type) {
filteredTemplates = synthesisTemplateList.filter(
(template) => template.type === type
);
}
const startIndex = page * size;
const endIndex = startIndex + parseInt(size);
const pageData = filteredTemplates.slice(startIndex, endIndex);
res.send({
code: "0",
msg: "Success",
data: {
content: pageData,
totalElements: filteredTemplates.length,
totalPages: Math.ceil(filteredTemplates.length / size),
size: parseInt(size),
number: parseInt(page)
}
});
});
// 创建合成模板
router.post(API.createSynthesisTemplateUsingPost, (req, res) => {
const newTemplate = {
...synthesisTemplateItem(),
...req.body,
id: Mock.Random.guid().replace(/[^a-zA-Z0-9]/g, ""),
enabled: true,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
};
synthesisTemplateList.push(newTemplate);
res.status(201).send({
code: "0",
msg: "Synthesis template created successfully",
data: newTemplate
});
});
// 获取合成模板详情
router.get(API.querySynthesisTemplateByIdUsingGet, (req, res) => {
const { templateId } = req.params;
const template = synthesisTemplateList.find((t) => t.id === templateId);
if (template) {
res.send({
code: "0",
msg: "Success",
data: template
});
} else {
res.status(404).send({
code: "1",
msg: "Synthesis template not found",
data: null
});
}
});
// 更新合成模板
router.put(API.updateSynthesisTemplateByIdUsingPut, (req, res) => {
const { templateId } = req.params;
const index = synthesisTemplateList.findIndex((t) => t.id === templateId);
if (index !== -1) {
synthesisTemplateList[index] = {
...synthesisTemplateList[index],
...req.body,
updatedAt: new Date().toISOString()
};
res.send({
code: "0",
msg: "Synthesis template updated successfully",
data: synthesisTemplateList[index]
});
} else {
res.status(404).send({
code: "1",
msg: "Synthesis template not found",
data: null
});
}
});
// 删除合成模板
router.delete(API.deleteSynthesisTemplateByIdUsingDelete, (req, res) => {
const { templateId } = req.params;
const index = synthesisTemplateList.findIndex((t) => t.id === templateId);
if (index !== -1) {
synthesisTemplateList.splice(index, 1);
res.send({
code: "0",
msg: "Synthesis template deleted successfully",
data: null
});
} else {
res.status(404).send({
code: "1",
msg: "Synthesis template not found",
data: null
});
}
});
// 获取合成任务列表
router.get(API.querySynthesisJobsUsingGet, (req, res) => {
const { page = 0, size = 20, status } = req.query;
let filteredJobs = synthesisJobList;
if (status) {
filteredJobs = synthesisJobList.filter((job) => job.status === status);
}
const startIndex = page * size;
const endIndex = startIndex + parseInt(size);
const pageData = filteredJobs.slice(startIndex, endIndex);
res.send({
code: "0",
msg: "Success",
data: {
content: pageData,
totalElements: filteredJobs.length,
totalPages: Math.ceil(filteredJobs.length / size),
size: parseInt(size),
number: parseInt(page)
}
});
});
// 创建合成任务
router.post(API.createSynthesisJobUsingPost, (req, res) => {
const { templateId } = req.body;
const template = synthesisTemplateList.find(t => t.id === templateId);
const newJob = {
...synthesisJobItem(),
...req.body,
id: Mock.Random.guid().replace(/[^a-zA-Z0-9]/g, ""),
templateId,
template: template ? {
id: template.id,
name: template.name,
type: template.type
} : null,
status: "PENDING",
progress: 0,
generatedCount: 0,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
};
synthesisJobList.push(newJob);
res.status(201).send({
code: "0",
msg: "Synthesis job created successfully",
data: newJob
});
});
// 获取合成任务详情
router.get(API.querySynthesisJobByIdUsingGet, (req, res) => {
const { jobId } = req.params;
const job = synthesisJobList.find((j) => j.id === jobId);
if (job) {
const template = synthesisTemplateList.find(t => t.id === job.templateId);
const jobDetail = {
...job,
template: template || null
};
res.send({
code: "0",
msg: "Success",
data: jobDetail
});
} else {
res.status(404).send({
code: "1",
msg: "Synthesis job not found",
data: null
});
}
});
// 删除合成任务
router.delete(API.deleteSynthesisJobByIdUsingDelete, (req, res) => {
const { jobId } = req.params;
const index = synthesisJobList.findIndex((j) => j.id === jobId);
if (index !== -1) {
synthesisJobList.splice(index, 1);
res.send({
code: "0",
msg: "Synthesis job deleted successfully",
data: null
});
} else {
res.status(404).send({
code: "1",
msg: "Synthesis job not found",
data: null
});
}
});
// 执行合成任务
router.post(API.executeSynthesisJobUsingPost, (req, res) => {
const { jobId } = req.params;
const job = synthesisJobList.find((j) => j.id === jobId);
if (job) {
job.status = "RUNNING";
job.startTime = new Date().toISOString();
job.updatedAt = new Date().toISOString();
// 模拟异步执行
setTimeout(() => {
job.status = Mock.Random.pick(["COMPLETED", "FAILED"]);
job.progress = job.status === "COMPLETED" ? 100 : Mock.Random.float(10, 90, 2, 2);
job.generatedCount = Math.floor((job.progress / 100) * job.targetCount);
job.endTime = new Date().toISOString();
}, Mock.Random.integer(2000, 5000));
res.send({
code: "0",
msg: "Synthesis job execution started",
data: {
executionId: Mock.Random.guid().replace(/[^a-zA-Z0-9]/g, ""),
status: "RUNNING",
message: "Job execution started successfully"
}
});
} else {
res.status(404).send({
code: "1",
msg: "Synthesis job not found",
data: null
});
}
});
// 指令调优数据合成
router.post(API.instructionTuningUsingPost, (req, res) => {
const { baseInstructions, targetDomain, count, modelConfig, parameters } = req.body;
const jobId = Mock.Random.guid().replace(/[^a-zA-Z0-9]/g, "");
const generatedInstructions = new Array(count).fill(null).map(() => generatedInstructionItem());
const statistics = {
totalGenerated: count,
averageQuality: Mock.Random.float(0.8, 0.95, 2, 2),
diversityScore: Mock.Random.float(0.7, 0.9, 2, 2)
};
res.send({
code: "0",
msg: "Instruction tuning completed successfully",
data: {
jobId,
generatedInstructions,
statistics
}
});
});
// COT蒸馏数据合成
router.post(API.cotDistillationUsingPost, (req, res) => {
const { sourceModel, targetFormat, examples, parameters } = req.body;
const jobId = Mock.Random.guid().replace(/[^a-zA-Z0-9]/g, "");
const processedCount = examples.length;
const successfulCount = Math.floor(processedCount * Mock.Random.float(0.85, 0.98, 2, 2));
const distilledData = new Array(successfulCount).fill(null).map(() => distilledCOTDataItem());
const statistics = {
totalProcessed: processedCount,
successfulDistilled: successfulCount,
averageConfidence: Mock.Random.float(0.8, 0.95, 2, 2)
};
res.send({
code: "0",
msg: "COT distillation completed successfully",
data: {
jobId,
distilledData,
statistics
}
});
});
// 获取合成任务统计信息
router.get("/api/v1/synthesis/statistics", (req, res) => {
const statistics = {
totalJobs: synthesisJobList.length,
completedJobs: synthesisJobList.filter(j => j.status === "COMPLETED").length,
runningJobs: synthesisJobList.filter(j => j.status === "RUNNING").length,
failedJobs: synthesisJobList.filter(j => j.status === "FAILED").length,
totalGenerated: synthesisJobList.reduce((sum, job) => sum + job.generatedCount, 0),
averageQuality: Mock.Random.float(0.8, 0.95, 2, 2),
templateTypeDistribution: {
"INSTRUCTION_TUNING": synthesisTemplateList.filter(t => t.type === "INSTRUCTION_TUNING").length,
"COT_DISTILLATION": synthesisTemplateList.filter(t => t.type === "COT_DISTILLATION").length,
"DIALOGUE_GENERATION": synthesisTemplateList.filter(t => t.type === "DIALOGUE_GENERATION").length,
"TEXT_AUGMENTATION": synthesisTemplateList.filter(t => t.type === "TEXT_AUGMENTATION").length,
"MULTIMODAL_SYNTHESIS": synthesisTemplateList.filter(t => t.type === "MULTIMODAL_SYNTHESIS").length,
"CUSTOM": synthesisTemplateList.filter(t => t.type === "CUSTOM").length
},
dailyGeneration: new Array(7).fill(null).map((_, index) => ({
date: Mock.Random.date("yyyy-MM-dd"),
count: Mock.Random.integer(100, 5000)
}))
};
res.send({
code: "0",
msg: "Success",
data: statistics
});
});
// 批量操作
router.post("/api/v1/synthesis/jobs/batch", (req, res) => {
const { action, jobIds } = req.body;
let successCount = 0;
let failedCount = 0;
jobIds.forEach(jobId => {
const job = synthesisJobList.find(j => j.id === jobId);
if (job) {
switch(action) {
case "DELETE":
const index = synthesisJobList.findIndex(j => j.id === jobId);
synthesisJobList.splice(index, 1);
successCount++;
break;
case "START":
job.status = "RUNNING";
job.startTime = new Date().toISOString();
successCount++;
break;
case "STOP":
job.status = "CANCELLED";
job.endTime = new Date().toISOString();
successCount++;
break;
}
} else {
failedCount++;
}
});
res.send({
code: "0",
msg: `Batch ${action.toLowerCase()} completed`,
data: {
total: jobIds.length,
success: successCount,
failed: failedCount
}
});
});
};

View File

@@ -0,0 +1,124 @@
const Mock = require("mockjs");
const API = require("../mock-apis.cjs");
// 算子标签数据
function labelItem() {
return {
id: Mock.Random.guid().replace(/[^a-zA-Z0-9]/g, ""),
name: Mock.Random.pick([
"数据清洗",
"特征选择",
"分类算法",
"聚类算法",
"回归分析",
"深度神经网络",
"卷积神经网络",
"循环神经网络",
"注意力机制",
"文本分析",
"图像处理",
"语音识别",
"推荐算法",
"异常检测",
"优化算法",
"集成学习",
"迁移学习",
"强化学习",
"联邦学习",
]),
usageCount: Mock.Random.integer(1, 500),
createdAt: Mock.Random.datetime("yyyy-MM-dd HH:mm:ss"),
};
}
const labelList = new Array(50).fill(null).map(labelItem);
module.exports = function (router) {
// 获取算子标签列表
router.get(API.queryLabelsUsingGet, (req, res) => {
const { page = 0, size = 20, keyword = "" } = req.query;
let filteredLabels = labelList;
if (keyword) {
filteredLabels = labelList.filter((label) =>
label.name.toLowerCase().includes(keyword.toLowerCase())
);
}
const startIndex = page * size;
const endIndex = startIndex + parseInt(size);
const pageData = filteredLabels.slice(startIndex, endIndex);
res.send({
code: "0",
msg: "Success",
data: {
content: pageData,
totalElements: filteredLabels.length,
totalPages: Math.ceil(filteredLabels.length / size),
size: parseInt(size),
number: parseInt(page),
},
});
});
// 创建标签
router.post(API.createLabelUsingPost, (req, res) => {
const { name } = req.body;
const newLabel = {
id: Mock.Random.guid().replace(/[^a-zA-Z0-9]/g, ""),
name,
usageCount: 0,
createdAt: new Date().toISOString(),
};
labelList.push(newLabel);
res.status(201).send({
code: "0",
msg: "Label created successfully",
data: newLabel,
});
});
// 批量删除标签
router.delete(API.deleteLabelsUsingDelete, (req, res) => {
const labelIds = req.body; // 数组形式的标签ID列表
let deletedCount = 0;
labelIds.forEach((labelId) => {
const index = labelList.findIndex((label) => label.id === labelId);
if (index !== -1) {
labelList.splice(index, 1);
deletedCount++;
}
});
res.status(204).send();
});
// 更新标签
router.put(API.updateLabelByIdUsingPut, (req, res) => {
const { id } = req.params;
const updates = req.body; // 数组形式的更新数据
updates.forEach((update) => {
const index = labelList.findIndex((label) => label.id === update.id);
if (index !== -1) {
labelList[index] = {
...labelList[index],
...update,
updatedAt: new Date().toISOString(),
};
}
});
res.send({
code: "0",
msg: "Labels updated successfully",
data: null,
});
});
};

View File

@@ -0,0 +1,58 @@
const express = require('express');
const fs = require('fs-extra');
const path = require('path');
const bodyParser = require('body-parser');
const { genExpressSession } = require('./mock-core/session-helper.cjs');
const {
setHeader,
sendJSON,
strongMatch,
errorHandle,
} = require('./mock-middleware/index.cjs');
const { loadAllMockModules } = require('./mock-core/module-loader.cjs');
const { log } = require('./mock-core/util.cjs');
const app = express();
const router = express.Router();
const argv = require('minimist')(process.argv.slice(2));
const deployUrl = argv['deploy-url'] || '/';
const deployPath = argv['deploy-path'] || '/';
const port = argv.port || 8002;
const env = argv.env || 'development';
// app静态文件实际目录
const deployAppPath = path.join(__dirname, deployPath);
preStartCheck(deployAppPath);
app.use(setHeader);
// 提供静态文件服务
app.use(deployUrl, express.static(deployAppPath));
app.use(bodyParser.json({limit: '1mb'}));
app.use(bodyParser.urlencoded({ extended: false, limit: '1mb' }));
app.use(sendJSON);
app.use(strongMatch);
app.use(genExpressSession());
const mockDir = path.join(__dirname, '/mock-seed');
loadAllMockModules(router, mockDir);
app.use(deployUrl, router);
app.use(errorHandle);
app.get('/', (req, res) => {
res.sendFile('default response', { root: deployAppPath });
});
app.listen(port, function() {
log(`Mock server is running at http://localhost:${port}${deployUrl} in ${env} mode`);
})
function preStartCheck(deployAppPath) {
if(!fs.existsSync(deployAppPath)) {
log(`Error: The path ${deployAppPath} does not exist. Please build the frontend application first.`, 'error');
process.exit(1);
}
}

View File

@@ -0,0 +1,22 @@
{
"restartable": "rs",
"ignore": [
".git",
"node_modules/**/node_modules",
"dist",
"build",
"*.test.js",
"*.spec.js"
],
"verbose": true,
"watch": ["*.cjs"],
"exec": "node --inspect=0.0.0.0:9229 mock.cjs",
"ext": "js,cjs,json",
"execMap": {
"js": "node --harmony"
},
"env": {
"NODE_ENV": "development"
},
"signal": "SIGTERM"
}

View File

@@ -0,0 +1,196 @@
export const mockOperators: Operator[] = [
{
id: 1,
name: "图像预处理算子",
version: "1.2.0",
description:
"支持图像缩放、裁剪、旋转、颜色空间转换等常用预处理操作,优化了内存使用和处理速度",
author: "张三",
category: "图像处理",
modality: ["image"],
type: "preprocessing",
tags: ["图像处理", "预处理", "缩放", "裁剪", "旋转"],
createdAt: "2024-01-15",
lastModified: "2024-01-23",
status: "active",
isFavorited: true,
downloads: 1247,
usage: 856,
framework: "PyTorch",
language: "Python",
size: "2.3MB",
dependencies: ["opencv-python", "pillow", "numpy"],
inputFormat: ["jpg", "png", "bmp", "tiff"],
outputFormat: ["jpg", "png", "tensor"],
performance: {
accuracy: 99.5,
speed: "50ms/image",
memory: "128MB",
},
},
{
id: 2,
name: "文本分词算子",
version: "2.1.3",
description:
"基于深度学习的中文分词算子,支持自定义词典,在医学文本上表现优异",
author: "李四",
category: "自然语言处理",
modality: ["text"],
type: "preprocessing",
tags: ["文本处理", "分词", "中文", "NLP", "医学"],
createdAt: "2024-01-10",
lastModified: "2024-01-20",
status: "active",
isFavorited: false,
downloads: 892,
usage: 634,
framework: "TensorFlow",
language: "Python",
size: "15.6MB",
dependencies: ["tensorflow", "jieba", "transformers"],
inputFormat: ["txt", "json", "csv"],
outputFormat: ["json", "txt"],
performance: {
accuracy: 96.8,
speed: "10ms/sentence",
memory: "256MB",
},
},
{
id: 3,
name: "音频特征提取",
version: "1.0.5",
description: "提取音频的MFCC、梅尔频谱、色度等特征,支持多种音频格式",
author: "王五",
category: "音频处理",
modality: ["audio"],
type: "preprocessing",
tags: ["音频处理", "特征提取", "MFCC", "频谱分析"],
createdAt: "2024-01-08",
lastModified: "2024-01-18",
status: "active",
isFavorited: true,
downloads: 456,
usage: 312,
framework: "PyTorch",
language: "Python",
size: "8.9MB",
dependencies: ["librosa", "scipy", "numpy"],
inputFormat: ["wav", "mp3", "flac", "m4a"],
outputFormat: ["npy", "json", "csv"],
performance: {
speed: "2x实时",
memory: "64MB",
},
},
{
id: 4,
name: "视频帧提取算子",
version: "1.3.2",
description: "高效的视频帧提取算子,支持关键帧检测和均匀采样",
author: "赵六",
category: "视频处理",
modality: ["video"],
type: "preprocessing",
tags: ["视频处理", "帧提取", "关键帧", "采样"],
createdAt: "2024-01-05",
lastModified: "2024-01-22",
status: "active",
isFavorited: false,
downloads: 723,
usage: 445,
framework: "OpenCV",
language: "Python",
size: "12.4MB",
dependencies: ["opencv-python", "ffmpeg-python"],
inputFormat: ["mp4", "avi", "mov", "mkv"],
outputFormat: ["jpg", "png", "npy"],
performance: {
speed: "30fps处理",
memory: "512MB",
},
},
{
id: 5,
name: "多模态融合算子",
version: "2.0.1",
description: "支持文本、图像、音频多模态数据融合的深度学习算子",
author: "孙七",
category: "多模态处理",
modality: ["text", "image", "audio"],
type: "training",
tags: ["多模态", "融合", "深度学习", "注意力机制"],
createdAt: "2024-01-12",
lastModified: "2024-01-21",
status: "beta",
isFavorited: false,
downloads: 234,
usage: 156,
framework: "PyTorch",
language: "Python",
size: "45.2MB",
dependencies: ["torch", "transformers", "torchvision", "torchaudio"],
inputFormat: ["json", "jpg", "wav"],
outputFormat: ["tensor", "json"],
performance: {
accuracy: 94.2,
speed: "100ms/sample",
memory: "2GB",
},
},
{
id: 6,
name: "模型推理加速",
version: "1.1.0",
description: "基于TensorRT的模型推理加速算子,支持多种深度学习框架",
author: "周八",
category: "模型优化",
modality: ["image", "text"],
type: "inference",
tags: ["推理加速", "TensorRT", "优化", "GPU"],
createdAt: "2024-01-03",
lastModified: "2024-01-19",
status: "active",
isFavorited: true,
downloads: 567,
usage: 389,
framework: "TensorRT",
language: "Python",
size: "23.7MB",
dependencies: ["tensorrt", "pycuda", "numpy"],
inputFormat: ["onnx", "pb", "pth"],
outputFormat: ["tensor", "json"],
performance: {
speed: "5x加速",
memory: "减少40%",
},
},
{
id: 7,
name: "数据增强算子",
version: "1.4.1",
description: "丰富的数据增强策略,包括几何变换、颜色变换、噪声添加等",
author: "吴九",
category: "数据增强",
modality: ["image"],
type: "preprocessing",
tags: ["数据增强", "几何变换", "颜色变换", "噪声"],
createdAt: "2024-01-01",
lastModified: "2024-01-17",
status: "active",
isFavorited: false,
downloads: 934,
usage: 678,
framework: "Albumentations",
language: "Python",
size: "6.8MB",
dependencies: ["albumentations", "opencv-python", "numpy"],
inputFormat: ["jpg", "png", "bmp"],
outputFormat: ["jpg", "png", "npy"],
performance: {
speed: "20ms/image",
memory: "32MB",
},
},
];

193
frontend/src/mock/ratio.tsx Normal file
View File

@@ -0,0 +1,193 @@
import type { RatioTask } from "@/pages/RatioTask/ratio";
export const mockRatioTasks: RatioTask[] = [
{
id: 1,
name: "多领域数据配比任务",
status: "completed",
progress: 100,
sourceDatasets: [
"orig_20250724_64082",
"financial_qa_dataset",
"medical_corpus",
],
targetCount: 10000,
generatedCount: 10000,
createdAt: "2025-01-24",
ratioType: "dataset",
estimatedTime: "已完成",
quality: 94,
ratioConfigs: [
{
id: "1",
name: "通用文本",
type: "dataset",
quantity: 4000,
percentage: 40,
source: "orig_20250724_64082",
},
{
id: "2",
name: "金融问答",
type: "dataset",
quantity: 3000,
percentage: 30,
source: "financial_qa_dataset",
},
{
id: "3",
name: "医疗语料",
type: "dataset",
quantity: 3000,
percentage: 30,
source: "medical_corpus",
},
],
},
{
id: 2,
name: "标签配比训练集",
status: "running",
progress: 68,
sourceDatasets: ["teacher_model_outputs", "image_text_pairs"],
targetCount: 8000,
generatedCount: 5440,
createdAt: "2025-01-25",
ratioType: "label",
estimatedTime: "剩余 12 分钟",
quality: 89,
ratioConfigs: [
{
id: "1",
name: "问答",
type: "label",
quantity: 2500,
percentage: 31.25,
source: "teacher_model_outputs_问答",
},
{
id: "2",
name: "推理",
type: "label",
quantity: 2000,
percentage: 25,
source: "teacher_model_outputs_推理",
},
{
id: "3",
name: "图像",
type: "label",
quantity: 1800,
percentage: 22.5,
source: "image_text_pairs_图像",
},
{
id: "4",
name: "描述",
type: "label",
quantity: 1700,
percentage: 21.25,
source: "image_text_pairs_描述",
},
],
},
{
id: 3,
name: "平衡数据集配比",
status: "failed",
progress: 25,
sourceDatasets: ["orig_20250724_64082", "financial_qa_dataset"],
targetCount: 5000,
generatedCount: 1250,
createdAt: "2025-01-25",
ratioType: "dataset",
errorMessage: "数据源连接失败,请检查数据集状态",
ratioConfigs: [
{
id: "1",
name: "通用文本",
type: "dataset",
quantity: 2500,
percentage: 50,
source: "orig_20250724_64082",
},
{
id: "2",
name: "金融问答",
type: "dataset",
quantity: 2500,
percentage: 50,
source: "financial_qa_dataset",
},
],
},
{
id: 4,
name: "文本分类配比任务",
status: "pending",
progress: 0,
sourceDatasets: ["text_classification_data", "sentiment_analysis_data"],
targetCount: 6000,
generatedCount: 0,
createdAt: "2025-01-26",
ratioType: "label",
estimatedTime: "预计 15 分钟",
ratioConfigs: [
{
id: "1",
name: "正面",
type: "label",
quantity: 2000,
percentage: 33.33,
source: "sentiment_analysis_data_正面",
},
{
id: "2",
name: "负面",
type: "label",
quantity: 2000,
percentage: 33.33,
source: "sentiment_analysis_data_负面",
},
{
id: "3",
name: "中性",
type: "label",
quantity: 2000,
percentage: 33.33,
source: "sentiment_analysis_data_中性",
},
],
},
{
id: 5,
name: "多模态数据配比",
status: "paused",
progress: 45,
sourceDatasets: ["image_caption_data", "video_description_data"],
targetCount: 12000,
generatedCount: 5400,
createdAt: "2025-01-23",
ratioType: "dataset",
estimatedTime: "已暂停",
quality: 91,
ratioConfigs: [
{
id: "1",
name: "图像描述",
type: "dataset",
quantity: 7000,
percentage: 58.33,
source: "image_caption_data",
},
{
id: "2",
name: "视频描述",
type: "dataset",
quantity: 5000,
percentage: 41.67,
source: "video_description_data",
},
],
},
];

View File

@@ -0,0 +1,209 @@
// Add mock files data
export const mockFiles = [
{ id: "file1", name: "dataset_part_001.jsonl", size: "2.5MB", type: "JSONL" },
{ id: "file2", name: "dataset_part_002.jsonl", size: "2.3MB", type: "JSONL" },
{ id: "file3", name: "dataset_part_003.jsonl", size: "2.7MB", type: "JSONL" },
{ id: "file4", name: "training_data.txt", size: "1.8MB", type: "TXT" },
{ id: "file5", name: "validation_set.csv", size: "856KB", type: "CSV" },
{ id: "file6", name: "test_samples.json", size: "1.2MB", type: "JSON" },
{ id: "file7", name: "raw_text_001.txt", size: "3.1MB", type: "TXT" },
{ id: "file8", name: "raw_text_002.txt", size: "2.9MB", type: "TXT" },
];
export const mockSynthesisTasks: SynthesisTask[] = [
{
id: 1,
name: "文字生成问答对_判断题",
type: "qa",
status: "completed",
progress: 100,
sourceDataset: "orig_20250724_64082",
targetCount: 1000,
generatedCount: 1000,
createdAt: "2025-01-20",
template: "判断题生成模板",
estimatedTime: "已完成",
quality: 95,
},
{
id: 2,
name: "知识蒸馏数据集",
type: "distillation",
status: "running",
progress: 65,
sourceDataset: "teacher_model_outputs",
targetCount: 5000,
generatedCount: 3250,
createdAt: "2025-01-22",
template: "蒸馏模板v2",
estimatedTime: "剩余 15 分钟",
quality: 88,
},
{
id: 3,
name: "多模态对话生成",
type: "multimodal",
status: "failed",
progress: 25,
sourceDataset: "image_text_pairs",
targetCount: 2000,
generatedCount: 500,
createdAt: "2025-01-23",
template: "多模态对话模板",
errorMessage: "模型API调用失败,请检查配置",
},
{
id: 4,
name: "金融问答数据生成",
type: "qa",
status: "pending",
progress: 0,
sourceDataset: "financial_qa_dataset",
targetCount: 800,
generatedCount: 0,
createdAt: "2025-01-24",
template: "金融问答模板",
estimatedTime: "等待开始",
quality: 0,
},
{
id: 5,
name: "医疗文本蒸馏",
type: "distillation",
status: "paused",
progress: 45,
sourceDataset: "medical_corpus",
targetCount: 3000,
generatedCount: 1350,
createdAt: "2025-01-21",
template: "医疗蒸馏模板",
estimatedTime: "已暂停",
quality: 92,
},
];
export const mockTemplates: Template[] = [
{
id: 1,
name: "判断题生成模板",
type: "preset",
category: "问答对生成",
prompt: `根据给定的文本内容,生成一个判断题。
文本内容:{text}
请按照以下格式生成:
1. 判断题:[基于文本内容的判断题]
2. 答案:[对/错]
3. 解释:[简要解释为什么这个答案是正确的]
要求:
- 判断题应该基于文本的核心内容
- 答案必须明确且有依据
- 解释要简洁清晰`,
variables: ["text"],
description: "根据文本内容生成判断题,适用于教育和培训场景",
usageCount: 156,
lastUsed: "2025-01-20",
quality: 95,
},
{
id: 2,
name: "选择题生成模板",
type: "preset",
category: "问答对生成",
prompt: `基于以下文本,创建一个多选题:
{text}
请按照以下格式生成:
问题:[基于文本的问题]
A. [选项A]
B. [选项B]
C. [选项C]
D. [选项D]
正确答案:[A/B/C/D]
解析:[详细解释]
要求:
- 问题要有一定难度
- 选项要有迷惑性
- 正确答案要有充分依据`,
variables: ["text"],
description: "生成多选题的标准模板,适用于考试和评估",
usageCount: 89,
lastUsed: "2025-01-19",
quality: 92,
},
{
id: 3,
name: "知识蒸馏模板",
type: "preset",
category: "蒸馏数据集",
prompt: `作为学生模型,学习教师模型的输出:
输入:{input}
教师输出:{teacher_output}
请模仿教师模型的推理过程和输出格式,生成相似质量的回答。
要求:
- 保持教师模型的推理逻辑
- 输出格式要一致
- 质量要接近教师模型水平`,
variables: ["input", "teacher_output"],
description: "用于知识蒸馏的模板,帮助小模型学习大模型的能力",
usageCount: 234,
lastUsed: "2025-01-22",
quality: 88,
},
{
id: 4,
name: "金融问答模板",
type: "custom",
category: "问答对生成",
prompt: `基于金融领域知识,生成专业问答对:
参考内容:{content}
生成格式:
问题:[专业的金融问题]
答案:[准确的专业回答]
关键词:[相关金融术语]
要求:
- 问题具有实用性
- 答案准确专业
- 符合金融行业标准`,
variables: ["content"],
description: "专门用于金融领域的问答对生成",
usageCount: 45,
lastUsed: "2025-01-18",
quality: 89,
},
{
id: 5,
name: "医疗蒸馏模板",
type: "custom",
category: "蒸馏数据集",
prompt: `医疗知识蒸馏模板:
原始医疗文本:{medical_text}
专家标注:{expert_annotation}
生成医疗知识点:
1. 核心概念:[提取关键医疗概念]
2. 临床意义:[说明临床应用价值]
3. 注意事项:[重要提醒和禁忌]
要求:
- 确保医疗信息准确性
- 遵循医疗伦理规范
- 适合医学教育使用`,
variables: ["medical_text", "expert_annotation"],
description: "医疗领域专用的知识蒸馏模板",
usageCount: 67,
lastUsed: "2025-01-21",
quality: 94,
},
];

View File

@@ -0,0 +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>
);
}

View File

@@ -0,0 +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>
);
}

View File

@@ -0,0 +1,713 @@
import { useState, useRef, useEffect } from "react";
import { Card, Button, Badge, Slider, message } from "antd";
import {
Play,
Pause,
Square,
SkipBack,
SkipForward,
Volume2,
VolumeX,
Scissors,
Save,
CheckCircle,
Trash2,
Edit,
Mic,
AudioWaveformIcon as Waveform,
} from "lucide-react";
interface AudioSegment {
id: string;
startTime: number;
endTime: number;
transcription: string;
label: string;
confidence?: number;
speaker?: string;
}
interface AudioAnnotationWorkspaceProps {
task: any;
currentFileIndex: number;
onSaveAndNext: () => void;
onSkipAndNext: () => void;
}
// 模拟音频数据
const mockAudioFiles = [
{
id: "1",
name: "interview_001.wav",
url: "/placeholder-audio.mp3", // 这里应该是实际的音频文件URL
duration: 180, // 3分钟
segments: [
{
id: "1",
startTime: 0,
endTime: 15,
transcription: "你好,欢迎参加今天的访谈。请先介绍一下自己。",
label: "问题",
confidence: 0.95,
speaker: "主持人",
},
{
id: "2",
startTime: 15,
endTime: 45,
transcription:
"大家好,我是张三,目前在一家科技公司担任产品经理,有五年的工作经验。",
label: "回答",
confidence: 0.88,
speaker: "受访者",
},
{
id: "3",
startTime: 45,
endTime: 60,
transcription: "很好,那么请谈谈你对人工智能发展的看法。",
label: "问题",
confidence: 0.92,
speaker: "主持人",
},
],
},
];
// 预定义标签
const audioLabels = [
{ name: "问题", color: "#3B82F6" },
{ name: "回答", color: "#10B981" },
{ name: "讨论", color: "#F59E0B" },
{ name: "总结", color: "#EF4444" },
{ name: "背景音", color: "#8B5CF6" },
{ name: "其他", color: "#6B7280" },
];
export default function AudioAnnotationWorkspace({
task,
currentFileIndex,
onSaveAndNext,
onSkipAndNext,
}: AudioAnnotationWorkspaceProps) {
const audioRef = useRef<HTMLAudioElement>(null);
const [currentAudio] = useState(mockAudioFiles[0]);
const [segments, setSegments] = useState<AudioSegment[]>(
currentAudio.segments
);
const [isPlaying, setIsPlaying] = useState(false);
const [currentTime, setCurrentTime] = useState(0);
const [duration, setDuration] = useState(currentAudio.duration);
const [volume, setVolume] = useState(1);
const [isMuted, setIsMuted] = useState(false);
const [selectedSegment, setSelectedSegment] = useState<string | null>(null);
const [isCreatingSegment, setIsCreatingSegment] = useState(false);
const [newSegmentStart, setNewSegmentStart] = useState(0);
const [editingSegment, setEditingSegment] = useState<AudioSegment | null>(
null
);
useEffect(() => {
const audio = audioRef.current;
if (!audio) return;
const updateTime = () => setCurrentTime(audio.currentTime);
const updateDuration = () => setDuration(audio.duration);
const handleEnded = () => setIsPlaying(false);
audio.addEventListener("timeupdate", updateTime);
audio.addEventListener("loadedmetadata", updateDuration);
audio.addEventListener("ended", handleEnded);
return () => {
audio.removeEventListener("timeupdate", updateTime);
audio.removeEventListener("loadedmetadata", updateDuration);
audio.removeEventListener("ended", handleEnded);
};
}, []);
const togglePlayPause = () => {
const audio = audioRef.current;
if (!audio) return;
if (isPlaying) {
audio.pause();
} else {
audio.play();
}
setIsPlaying(!isPlaying);
};
const handleSeek = (time: number) => {
const audio = audioRef.current;
if (!audio) return;
audio.currentTime = time;
setCurrentTime(time);
};
const handleVolumeChange = (value: number[]) => {
const audio = audioRef.current;
if (!audio) return;
const newVolume = value[0];
audio.volume = newVolume;
setVolume(newVolume);
setIsMuted(newVolume === 0);
};
const toggleMute = () => {
const audio = audioRef.current;
if (!audio) return;
if (isMuted) {
audio.volume = volume;
setIsMuted(false);
} else {
audio.volume = 0;
setIsMuted(true);
}
};
const startCreatingSegment = () => {
setIsCreatingSegment(true);
setNewSegmentStart(currentTime);
toast({
title: "开始创建片段",
description: `片段起始时间: ${formatTime(currentTime)}`,
});
};
const finishCreatingSegment = () => {
if (!isCreatingSegment) return;
const newSegment: AudioSegment = {
id: Date.now().toString(),
startTime: newSegmentStart,
endTime: currentTime,
transcription: "",
label: audioLabels[0].name,
speaker: "",
};
setSegments([...segments, newSegment]);
setIsCreatingSegment(false);
setEditingSegment(newSegment);
toast({
title: "片段已创建",
description: `时长: ${formatTime(currentTime - newSegmentStart)}`,
});
};
const deleteSegment = (id: string) => {
setSegments(segments.filter((s) => s.id !== id));
setSelectedSegment(null);
toast({
title: "片段已删除",
description: "音频片段已被删除",
});
};
const updateSegment = (updatedSegment: AudioSegment) => {
setSegments(
segments.map((s) => (s.id === updatedSegment.id ? updatedSegment : s))
);
setEditingSegment(null);
toast({
title: "片段已更新",
description: "转录内容已保存",
});
};
const playSegment = (segment: AudioSegment) => {
handleSeek(segment.startTime);
setSelectedSegment(segment.id);
const audio = audioRef.current;
if (!audio) return;
audio.play();
setIsPlaying(true);
// 在片段结束时暂停
const checkEnd = () => {
if (audio.currentTime >= segment.endTime) {
audio.pause();
setIsPlaying(false);
audio.removeEventListener("timeupdate", checkEnd);
}
};
audio.addEventListener("timeupdate", checkEnd);
};
const formatTime = (seconds: number) => {
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins.toString().padStart(2, "0")}:${secs
.toString()
.padStart(2, "0")}`;
};
const getSegmentColor = (label: string) => {
const labelConfig = audioLabels.find((l) => l.name === label);
return labelConfig?.color || "#6B7280";
};
return (
<div className="flex-1 flex flex-col">
{/* Audio Player */}
<div className="border-b bg-white p-4">
<div className="space-y-4">
{/* Audio Element */}
<audio ref={audioRef} src={currentAudio.url} preload="metadata" />
{/* Player Controls */}
<div className="flex items-center justify-center space-x-4">
<Button
variant="outline"
size="sm"
onClick={() => handleSeek(Math.max(0, currentTime - 10))}
>
<SkipBack className="w-4 h-4" />
</Button>
<Button
onClick={togglePlayPause}
size="lg"
className="bg-blue-600 hover:bg-blue-700"
>
{isPlaying ? (
<Pause className="w-6 h-6" />
) : (
<Play className="w-6 h-6" />
)}
</Button>
<Button
variant="outline"
size="sm"
onClick={() => handleSeek(Math.min(duration, currentTime + 10))}
>
<SkipForward className="w-4 h-4" />
</Button>
</div>
{/* Timeline */}
<div className="space-y-2">
<div className="flex items-center justify-between text-sm text-gray-600">
<span>{formatTime(currentTime)}</span>
<span>{formatTime(duration)}</span>
</div>
<div className="relative">
<Slider
value={[currentTime]}
max={duration}
step={0.1}
onValueChange={(value) => handleSeek(value[0])}
className="w-full"
/>
{/* Segment Visualization */}
<div className="absolute top-0 left-0 w-full h-full pointer-events-none">
{segments.map((segment) => {
const left = (segment.startTime / duration) * 100;
const width =
((segment.endTime - segment.startTime) / duration) * 100;
return (
<div
key={segment.id}
className="absolute top-0 h-full opacity-30 rounded"
style={{
left: `${left}%`,
width: `${width}%`,
backgroundColor: getSegmentColor(segment.label),
}}
/>
);
})}
</div>
{/* Current Creating Segment */}
{isCreatingSegment && (
<div
className="absolute top-0 h-full bg-red-400 opacity-50 rounded"
style={{
left: `${(newSegmentStart / duration) * 100}%`,
width: `${
((currentTime - newSegmentStart) / duration) * 100
}%`,
}}
/>
)}
</div>
</div>
{/* Volume Control */}
<div className="flex items-center justify-center space-x-2">
<Button variant="ghost" size="sm" onClick={toggleMute}>
{isMuted ? (
<VolumeX className="w-4 h-4" />
) : (
<Volume2 className="w-4 h-4" />
)}
</Button>
<Slider
value={[isMuted ? 0 : volume]}
max={1}
step={0.1}
onValueChange={handleVolumeChange}
className="w-24"
/>
</div>
{/* Annotation Controls */}
<div className="flex items-center justify-center space-x-2">
{isCreatingSegment ? (
<Button
onClick={finishCreatingSegment}
className="bg-green-600 hover:bg-green-700"
>
<Square className="w-4 h-4 mr-2" />
</Button>
) : (
<Button onClick={startCreatingSegment} variant="outline">
<Scissors className="w-4 h-4 mr-2" />
</Button>
)}
</div>
</div>
</div>
{/* Main Content */}
<div className="flex-1 flex">
{/* Segments List */}
<div className="w-96 border-r bg-gray-50 p-4">
<div className="space-y-4">
<div className="flex items-center justify-between">
<h3 className="font-medium"></h3>
<Badge variant="outline">{segments.length} </Badge>
</div>
<ScrollArea className="h-96">
<div className="space-y-2">
{segments.map((segment) => (
<Card
key={segment.id}
className={`cursor-pointer transition-colors ${
selectedSegment === segment.id
? "border-blue-500 bg-blue-50"
: "hover:bg-gray-50"
}`}
onClick={() => setSelectedSegment(segment.id)}
>
<CardContent className="p-3">
<div className="space-y-2">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<div
className="w-3 h-3 rounded"
style={{
backgroundColor: getSegmentColor(segment.label),
}}
/>
<span className="text-sm font-medium">
{segment.label}
</span>
</div>
<div className="flex items-center space-x-1">
<Button
variant="ghost"
size="sm"
className="p-1 h-auto"
onClick={(e) => {
e.stopPropagation();
playSegment(segment);
}}
>
<Play className="w-3 h-3" />
</Button>
<Button
variant="ghost"
size="sm"
className="p-1 h-auto"
onClick={(e) => {
e.stopPropagation();
setEditingSegment(segment);
}}
>
<Edit className="w-3 h-3" />
</Button>
<Button
variant="ghost"
size="sm"
className="p-1 h-auto text-red-500"
onClick={(e) => {
e.stopPropagation();
deleteSegment(segment.id);
}}
>
<Trash2 className="w-3 h-3" />
</Button>
</div>
</div>
<div className="text-xs text-gray-500">
{formatTime(segment.startTime)} -{" "}
{formatTime(segment.endTime)}
{segment.speaker && ` | ${segment.speaker}`}
</div>
<p className="text-sm text-gray-700 line-clamp-2">
{segment.transcription || "未转录"}
</p>
{segment.confidence && (
<div className="flex items-center space-x-2">
<span className="text-xs text-gray-500">
:
</span>
<Badge variant="outline" className="text-xs">
{(segment.confidence * 100).toFixed(1)}%
</Badge>
</div>
)}
</div>
</CardContent>
</Card>
))}
</div>
</ScrollArea>
</div>
</div>
{/* Transcription Editor */}
<div className="flex-1 p-6">
{editingSegment ? (
<Card>
<CardHeader>
<CardTitle className="flex items-center space-x-2">
<Mic className="w-5 h-5" />
<span></span>
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-sm font-medium"></label>
<Input
value={formatTime(editingSegment.startTime)}
readOnly
className="mt-1"
/>
</div>
<div>
<label className="text-sm font-medium"></label>
<Input
value={formatTime(editingSegment.endTime)}
readOnly
className="mt-1"
/>
</div>
</div>
<div>
<label className="text-sm font-medium"></label>
<select
value={editingSegment.label}
onChange={(e) =>
setEditingSegment({
...editingSegment,
label: e.target.value,
})
}
className="w-full mt-1 px-3 py-2 border rounded-md"
>
{audioLabels.map((label) => (
<option key={label.name} value={label.name}>
{label.name}
</option>
))}
</select>
</div>
<div>
<label className="text-sm font-medium"></label>
<Input
value={editingSegment.speaker || ""}
onChange={(e) =>
setEditingSegment({
...editingSegment,
speaker: e.target.value,
})
}
placeholder="输入说话人名称"
className="mt-1"
/>
</div>
<div>
<label className="text-sm font-medium"></label>
<Textarea
value={editingSegment.transcription}
onChange={(e) =>
setEditingSegment({
...editingSegment,
transcription: e.target.value,
})
}
placeholder="输入或编辑转录内容..."
rows={6}
className="mt-1"
/>
</div>
<div className="flex items-center justify-end space-x-2">
<Button
variant="outline"
onClick={() => setEditingSegment(null)}
>
</Button>
<Button
onClick={() => updateSegment(editingSegment)}
className="bg-blue-600 hover:bg-blue-700"
>
<Save className="w-4 h-4 mr-2" />
</Button>
</div>
</CardContent>
</Card>
) : selectedSegment ? (
<Card>
<CardHeader>
<CardTitle className="flex items-center space-x-2">
<Waveform className="w-5 h-5" />
<span></span>
</CardTitle>
</CardHeader>
<CardContent>
{(() => {
const segment = segments.find(
(s) => s.id === selectedSegment
);
if (!segment) return null;
return (
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<span className="text-sm text-gray-500">
</span>
<p className="font-medium">
{formatTime(segment.startTime)} -{" "}
{formatTime(segment.endTime)}
</p>
</div>
<div>
<span className="text-sm text-gray-500"></span>
<p className="font-medium">
{formatTime(segment.endTime - segment.startTime)}
</p>
</div>
</div>
<div>
<span className="text-sm text-gray-500"></span>
<div className="flex items-center space-x-2 mt-1">
<div
className="w-3 h-3 rounded"
style={{
backgroundColor: getSegmentColor(segment.label),
}}
/>
<span className="font-medium">{segment.label}</span>
</div>
</div>
{segment.speaker && (
<div>
<span className="text-sm text-gray-500"></span>
<p className="font-medium">{segment.speaker}</p>
</div>
)}
<div>
<span className="text-sm text-gray-500"></span>
<p className="mt-1 p-3 bg-gray-50 rounded text-sm">
{segment.transcription || "暂无转录内容"}
</p>
</div>
{segment.confidence && (
<div>
<span className="text-sm text-gray-500"></span>
<p className="font-medium">
{(segment.confidence * 100).toFixed(1)}%
</p>
</div>
)}
<div className="flex items-center space-x-2">
<Button
onClick={() => playSegment(segment)}
variant="outline"
>
<Play className="w-4 h-4 mr-2" />
</Button>
<Button
onClick={() => setEditingSegment(segment)}
variant="outline"
>
<Edit className="w-4 h-4 mr-2" />
</Button>
</div>
</div>
);
})()}
</CardContent>
</Card>
) : (
<div className="flex-1 flex items-center justify-center">
<div className="text-center">
<Mic 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 mb-4">
</p>
<div className="space-y-2 text-sm text-gray-600">
<p> "创建片段"</p>
<p> </p>
<p> 使</p>
</div>
</div>
</div>
)}
</div>
</div>
{/* Bottom Actions */}
<div className="border-t bg-white p-4">
<div className="flex items-center justify-between">
<div className="text-sm text-gray-600">
: {currentAudio.name} | : {segments.length} | :{" "}
{formatTime(duration)}
</div>
<div className="flex items-center space-x-2">
<Button onClick={onSkipAndNext} variant="outline">
</Button>
<Button
onClick={onSaveAndNext}
className="bg-green-600 hover:bg-green-700"
>
<CheckCircle className="w-4 h-4 mr-2" />
</Button>
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,617 @@
import type React from "react";
import { useState, useRef, useEffect } from "react";
import { Button, Badge, Checkbox, message } from "antd";
import {
Square,
Circle,
MousePointer,
ZoomIn,
ZoomOut,
RotateCcw,
ArrowLeft,
ArrowRight,
MoreHorizontal,
} from "lucide-react";
interface Annotation {
id: string;
type: "rectangle" | "circle" | "polygon";
label: string;
color: string;
coordinates: number[];
visible: boolean;
}
interface ImageAnnotationWorkspaceProps {
task: any;
currentFileIndex: number;
onSaveAndNext: () => void;
onSkipAndNext: () => void;
}
// 模拟医学图像数据
const mockMedicalImages = [
{
id: "1",
name: "2024-123456",
thumbnail: "/placeholder.svg?height=60&width=60&text=Slide1",
url: "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/img_v3_02oi_e6dd5540-9ca4-4277-ad2b-4debaa1c8ddg.jpg-oibLbUmFpZMkLTmwZB7lT1UWKFlOLA.jpeg",
},
{
id: "2",
name: "2024-234567",
thumbnail: "/placeholder.svg?height=60&width=60&text=Slide2",
url: "/placeholder.svg?height=600&width=800&text=Medical Image 2",
},
{
id: "3",
name: "2025-345678",
thumbnail: "/placeholder.svg?height=60&width=60&text=Slide3",
url: "/placeholder.svg?height=600&width=800&text=Medical Image 3",
},
{
id: "4",
name: "1234-123456",
thumbnail: "/placeholder.svg?height=60&width=60&text=Slide4",
url: "/placeholder.svg?height=600&width=800&text=Medical Image 4",
},
{
id: "5",
name: "2025-456789",
thumbnail: "/placeholder.svg?height=60&width=60&text=Slide5",
url: "/placeholder.svg?height=600&width=800&text=Medical Image 5",
},
{
id: "6",
name: "2025-567890",
thumbnail: "/placeholder.svg?height=60&width=60&text=Slide6",
url: "/placeholder.svg?height=600&width=800&text=Medical Image 6",
},
{
id: "7",
name: "2025-678901",
thumbnail: "/placeholder.svg?height=60&width=60&text=Slide7",
url: "/placeholder.svg?height=600&width=800&text=Medical Image 7",
},
{
id: "8",
name: "2025-789012",
thumbnail: "/placeholder.svg?height=60&width=60&text=Slide8",
url: "/placeholder.svg?height=600&width=800&text=Medical Image 8",
},
{
id: "9",
name: "2025-890123",
thumbnail: "/placeholder.svg?height=60&width=60&text=Slide9",
url: "/placeholder.svg?height=600&width=800&text=Medical Image 9",
},
{
id: "10",
name: "2025-901234",
thumbnail: "/placeholder.svg?height=60&width=60&text=Slide10",
url: "/placeholder.svg?height=600&width=800&text=Medical Image 10",
},
];
// 医学标注选项
const medicalAnnotationOptions = [
{
id: "tumor_present",
label: "是否有肿瘤",
type: "radio",
options: ["是", "否"],
},
{
id: "tumor_type",
label: "肿瘤形成",
type: "checkbox",
options: ["腺管形成"],
},
{ id: "grade_1", label: "1级", type: "checkbox", options: ["1[x]"] },
{ id: "grade_2", label: "2级", type: "checkbox", options: ["2[x]"] },
{ id: "remarks", label: "备注", type: "textarea" },
{
id: "nuclear_polymorphism",
label: "核多形性",
type: "checkbox",
options: ["核分裂象"],
},
{
id: "histological_type",
label: "组织学类型",
type: "checkbox",
options: ["1[b]", "2[y]", "3[t]"],
},
{
id: "small_time_lesion",
label: "小时病位置[3]",
type: "checkbox",
options: ["1[b]", "2[y]", "3[t]"],
},
{
id: "ductal_position",
label: "导管原位置[4]",
type: "checkbox",
options: ["1[o]", "2[p]", "3[t]"],
},
{
id: "ductal_position_large",
label: "导管原位置件大于腺分",
type: "checkbox",
options: ["腺分裂象"],
},
{
id: "mitosis",
label: "化[5]",
type: "checkbox",
options: ["1[o]", "2[p]", "3[t]"],
},
{
id: "original_position",
label: "原位实性乳头状[6]",
type: "checkbox",
options: ["1[o]", "2[p]", "3[t]"],
},
{
id: "infiltrating_lesion",
label: "浸润性病(非特殊型)[7]",
type: "checkbox",
options: ["1[o]", "2[p]", "3[t]"],
},
{
id: "infiltrating_small",
label: "浸润性小叶癌[8]",
type: "checkbox",
options: ["脉管侵犯"],
},
{
id: "infiltrating_real",
label: "浸润实性乳头状癌[9]",
type: "checkbox",
options: ["1[o]", "2[p]", "3[t]"],
},
{
id: "other_lesion",
label: "其他病[0]",
type: "checkbox",
options: ["+[k]"],
},
];
export default function ImageAnnotationWorkspace({
currentFileIndex,
}: ImageAnnotationWorkspaceProps) {
const canvasRef = useRef<HTMLCanvasElement>(null);
const [selectedImageIndex, setSelectedImageIndex] = useState(
currentFileIndex || 0
);
const [currentImage, setCurrentImage] = useState(
mockMedicalImages[selectedImageIndex]
);
const [annotations, setAnnotations] = useState<Annotation[]>([]);
const [selectedTool, setSelectedTool] = useState<
"select" | "rectangle" | "circle"
>("select");
const [isDrawing, setIsDrawing] = useState(false);
const [startPoint, setStartPoint] = useState({ x: 0, y: 0 });
const [zoom, setZoom] = useState(1);
const [pan, setPan] = useState({ x: 0, y: 0 });
const [selectedAnnotation, setSelectedAnnotation] = useState<string | null>(
null
);
const [annotationValues, setAnnotationValues] = useState<Record<string, any>>(
{}
);
useEffect(() => {
setCurrentImage(mockMedicalImages[selectedImageIndex]);
drawCanvas();
}, [selectedImageIndex, annotations, zoom, pan]);
const drawCanvas = () => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
ctx.clearRect(0, 0, canvas.width, canvas.height);
const img = new Image();
img.crossOrigin = "anonymous";
img.onload = () => {
ctx.save();
ctx.scale(zoom, zoom);
ctx.translate(pan.x, pan.y);
ctx.drawImage(img, 0, 0, canvas.width / zoom, canvas.height / zoom);
// 绘制标注
annotations.forEach((annotation) => {
if (!annotation.visible) return;
ctx.strokeStyle = annotation.color;
ctx.fillStyle = annotation.color + "20";
ctx.lineWidth = 2;
if (annotation.type === "rectangle") {
const [x, y, width, height] = annotation.coordinates;
ctx.strokeRect(x, y, width, height);
ctx.fillRect(x, y, width, height);
} else if (annotation.type === "circle") {
const [centerX, centerY, radius] = annotation.coordinates;
ctx.beginPath();
ctx.arc(centerX, centerY, radius, 0, 2 * Math.PI);
ctx.stroke();
ctx.fill();
}
if (selectedAnnotation === annotation.id) {
ctx.strokeStyle = "#FF0000";
ctx.lineWidth = 3;
ctx.setLineDash([5, 5]);
if (annotation.type === "rectangle") {
const [x, y, width, height] = annotation.coordinates;
ctx.strokeRect(x - 2, y - 2, width + 4, height + 4);
} else if (annotation.type === "circle") {
const [centerX, centerY, radius] = annotation.coordinates;
ctx.beginPath();
ctx.arc(centerX, centerY, radius + 2, 0, 2 * Math.PI);
ctx.stroke();
}
ctx.setLineDash([]);
}
});
ctx.restore();
};
img.src = currentImage.url;
};
const handleCanvasMouseDown = (e: React.MouseEvent<HTMLCanvasElement>) => {
const canvas = canvasRef.current;
if (!canvas) return;
const rect = canvas.getBoundingClientRect();
const x = (e.clientX - rect.left - pan.x) / zoom;
const y = (e.clientY - rect.top - pan.y) / zoom;
if (selectedTool === "rectangle" || selectedTool === "circle") {
setIsDrawing(true);
setStartPoint({ x, y });
} else if (selectedTool === "select") {
const clickedAnnotation = annotations.find((annotation) => {
if (annotation.type === "rectangle") {
const [ax, ay, width, height] = annotation.coordinates;
return x >= ax && x <= ax + width && y >= ay && y <= ay + height;
} else if (annotation.type === "circle") {
const [centerX, centerY, radius] = annotation.coordinates;
const distance = Math.sqrt((x - centerX) ** 2 + (y - centerY) ** 2);
return distance <= radius;
}
return false;
});
setSelectedAnnotation(clickedAnnotation?.id || null);
}
};
const handleCanvasMouseMove = (e: React.MouseEvent<HTMLCanvasElement>) => {
if (!isDrawing) return;
const canvas = canvasRef.current;
if (!canvas) return;
const rect = canvas.getBoundingClientRect();
const x = (e.clientX - rect.left - pan.x) / zoom;
const y = (e.clientY - rect.top - pan.y) / zoom;
drawCanvas();
const ctx = canvas.getContext("2d");
if (!ctx) return;
ctx.save();
ctx.scale(zoom, zoom);
ctx.translate(pan.x, pan.y);
ctx.strokeStyle = "#3B82F6";
ctx.lineWidth = 2;
ctx.setLineDash([5, 5]);
if (selectedTool === "rectangle") {
const width = x - startPoint.x;
const height = y - startPoint.y;
ctx.strokeRect(startPoint.x, startPoint.y, width, height);
} else if (selectedTool === "circle") {
const radius = Math.sqrt(
(x - startPoint.x) ** 2 + (y - startPoint.y) ** 2
);
ctx.beginPath();
ctx.arc(startPoint.x, startPoint.y, radius, 0, 2 * Math.PI);
ctx.stroke();
}
ctx.restore();
};
const handleCanvasMouseUp = (e: React.MouseEvent<HTMLCanvasElement>) => {
if (!isDrawing) return;
const canvas = canvasRef.current;
if (!canvas) return;
const rect = canvas.getBoundingClientRect();
const x = (e.clientX - rect.left - pan.x) / zoom;
const y = (e.clientY - rect.top - pan.y) / zoom;
let coordinates: number[] = [];
if (selectedTool === "rectangle") {
const width = x - startPoint.x;
const height = y - startPoint.y;
coordinates = [startPoint.x, startPoint.y, width, height];
} else if (selectedTool === "circle") {
const radius = Math.sqrt(
(x - startPoint.x) ** 2 + (y - startPoint.y) ** 2
);
coordinates = [startPoint.x, startPoint.y, radius];
}
if (coordinates.length > 0) {
const newAnnotation: Annotation = {
id: Date.now().toString(),
type: selectedTool as "rectangle" | "circle",
label: "标注",
color: "#3B82F6",
coordinates,
visible: true,
};
setAnnotations([...annotations, newAnnotation]);
}
setIsDrawing(false);
};
const handleAnnotationValueChange = (optionId: string, value: any) => {
setAnnotationValues((prev) => ({
...prev,
[optionId]: value,
}));
};
const handleUpdate = () => {
message({
title: "标注已更新",
description: "医学标注信息已保存",
});
};
return (
<div className="h-screen flex">
{/* Left Sidebar - Image List */}
<div className="w-80 border-r bg-white">
{/* Header */}
<div className="p-4 border-b bg-gray-50">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<Badge variant="outline">image</Badge>
<Badge className="bg-blue-100 text-blue-800">img</Badge>
</div>
<div className="flex items-center space-x-2">
<span className="text-sm text-gray-600">case_id</span>
<span className="text-sm font-mono">#13754</span>
<Button variant="ghost" size="sm">
<MoreHorizontal className="w-4 h-4" />
</Button>
</div>
</div>
<div className="flex items-center justify-between mt-2">
<div className="flex items-center space-x-2">
<div className="w-6 h-6 bg-purple-500 rounded-full flex items-center justify-center text-white text-xs">
DE
</div>
<span className="text-sm text-gray-600">de #14803</span>
</div>
<span className="text-xs text-gray-500">11 days ago</span>
</div>
</div>
{/* Image List */}
<div className="p-2">
{mockMedicalImages.map((image, index) => (
<div
key={image.id}
className={`flex items-center p-3 mb-2 rounded-lg cursor-pointer transition-colors ${
selectedImageIndex === index
? "bg-blue-50 border border-blue-200"
: "hover:bg-gray-50"
}`}
onClick={() => setSelectedImageIndex(index)}
>
<div className="w-8 h-8 bg-gray-200 rounded flex items-center justify-center text-sm font-medium mr-3">
{index + 1}
</div>
<img
src={image.thumbnail || "/placeholder.svg"}
alt={`Slide ${index + 1}`}
className="w-12 h-12 rounded border mr-3"
/>
<div className="flex-1">
<div className="font-medium text-sm">{image.name}</div>
</div>
</div>
))}
</div>
</div>
{/* Main Content Area */}
<div className="flex-1 flex flex-col">
{/* Main Image Display */}
<div className="flex-1 p-4">
<div className="h-full flex flex-col">
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold">WSI图像预览</h2>
<div className="flex items-center space-x-2">
<div className="text-sm text-gray-600">
: <span className="font-mono">1234-123456</span>
</div>
<div className="text-sm text-gray-600">
: <span></span>
</div>
</div>
</div>
<div className="flex-1 border rounded-lg overflow-hidden bg-gray-100 relative">
<canvas
ref={canvasRef}
width={800}
height={600}
className="w-full h-full object-contain cursor-crosshair"
onMouseDown={handleCanvasMouseDown}
onMouseMove={handleCanvasMouseMove}
onMouseUp={handleCanvasMouseUp}
/>
{/* Zoom Controls */}
<div className="absolute bottom-4 left-4 flex items-center space-x-2 bg-white rounded-lg shadow-lg p-2">
<Button onClick={() => setZoom(Math.max(zoom / 1.2, 0.1))}>
<ZoomOut className="w-4 h-4" />
</Button>
<span className="text-sm px-2">{Math.round(zoom * 100)}%</span>
<Button onClick={() => setZoom(Math.min(zoom * 1.2, 5))}>
<ZoomIn className="w-4 h-4" />
</Button>
<Button
onClick={() => {
setZoom(1);
setPan({ x: 0, y: 0 });
}}
>
<RotateCcw className="w-4 h-4" />
</Button>
</div>
{/* Tool Selection */}
<div className="absolute top-4 left-4 flex items-center space-x-2 bg-white rounded-lg shadow-lg p-2">
<Button
variant={selectedTool === "select" ? "default" : "outline"}
size="sm"
onClick={() => setSelectedTool("select")}
>
<MousePointer className="w-4 h-4" />
</Button>
<Button
variant={selectedTool === "rectangle" ? "default" : "outline"}
size="sm"
onClick={() => setSelectedTool("rectangle")}
>
<Square className="w-4 h-4" />
</Button>
<Button
variant={selectedTool === "circle" ? "default" : "outline"}
size="sm"
onClick={() => setSelectedTool("circle")}
>
<Circle className="w-4 h-4" />
</Button>
</div>
</div>
{/* Navigation Controls */}
<div className="flex items-center justify-center mt-4 space-x-2">
<Button>
<ArrowLeft className="w-4 h-4" />
</Button>
<Button>
<ArrowRight className="w-4 h-4" />
</Button>
<span className="text-sm text-gray-600 mx-4"></span>
</div>
</div>
</div>
</div>
{/* Right Sidebar - Annotation Panel */}
<div className="w-80 border-l bg-gray-50 p-4">
<div className="space-y-4">
<div>
<h3 className="font-semibold text-lg mb-4"></h3>
<div className="space-y-4">
{medicalAnnotationOptions.map((option) => (
<div key={option.id} className="space-y-2">
<span className="text-sm font-medium">{option.label}</span>
{option.type === "radio" && (
<div className="space-y-1">
{option.options?.map((opt) => (
<div key={opt} className="flex items-center space-x-2">
<input
type="radio"
name={option.id}
value={opt}
onChange={(e) =>
handleAnnotationValueChange(
option.id,
e.target.value
)
}
className="w-4 h-4"
/>
<span className="text-sm">{opt}</span>
</div>
))}
</div>
)}
{option.type === "checkbox" && (
<div className="space-y-1">
{option.options?.map((opt) => (
<div key={opt} className="flex items-center space-x-2">
<Checkbox
checked={
annotationValues[`${option.id}_${opt}`] || false
}
onChange={(checked) =>
handleAnnotationValueChange(
`${option.id}_${opt}`,
checked
)
}
/>
<span className="text-sm">{opt}</span>
</div>
))}
</div>
)}
{option.type === "textarea" && (
<textarea
className="w-full p-2 border rounded-md text-sm resize-none"
rows={3}
placeholder={`请输入${option.label}`}
value={annotationValues[option.id] || ""}
onChange={(e) =>
handleAnnotationValueChange(option.id, e.target.value)
}
/>
)}
</div>
))}
</div>
</div>
<div className="pt-4 border-t">
<Button
onClick={handleUpdate}
className="w-full bg-amber-600 hover:bg-amber-700"
>
Update
</Button>
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +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>
);
}

View File

@@ -0,0 +1,688 @@
import type React from "react";
import { useState, useRef, useEffect } from "react";
import { Card, Button, Badge, Slider, message } from "antd";
import {
Play,
Pause,
Square,
SkipBack,
SkipForward,
Volume2,
VolumeX,
MousePointer,
CheckCircle,
Trash2,
Eye,
EyeOff,
Target,
Maximize,
} from "lucide-react";
interface VideoAnnotation {
id: string;
frameTime: number;
type: "rectangle" | "point" | "polygon";
coordinates: number[];
label: string;
color: string;
trackId?: string;
visible: boolean;
}
interface VideoTrack {
id: string;
label: string;
color: string;
annotations: VideoAnnotation[];
startTime: number;
endTime: number;
}
interface VideoAnnotationWorkspaceProps {
task: any;
currentFileIndex: number;
onSaveAndNext: () => void;
onSkipAndNext: () => void;
}
// 模拟视频数据
const mockVideoFiles = [
{
id: "1",
name: "traffic_scene_001.mp4",
url: "/placeholder-video.mp4", // 这里应该是实际的视频文件URL
duration: 120, // 2分钟
fps: 30,
width: 1920,
height: 1080,
},
];
// 预定义标签
const videoLabels = [
{ name: "车辆", color: "#3B82F6" },
{ name: "行人", color: "#10B981" },
{ name: "自行车", color: "#F59E0B" },
{ name: "交通灯", color: "#EF4444" },
{ name: "路标", color: "#8B5CF6" },
{ name: "其他", color: "#6B7280" },
];
export default function VideoAnnotationWorkspace({
task,
currentFileIndex,
onSaveAndNext,
onSkipAndNext,
}: VideoAnnotationWorkspaceProps) {
const [messageApi, contextHolder] = message.useMessage();
const videoRef = useRef<HTMLVideoElement>(null);
const canvasRef = useRef<HTMLCanvasElement>(null);
const [currentVideo] = useState(mockVideoFiles[0]);
const [tracks, setTracks] = useState<VideoTrack[]>([]);
const [isPlaying, setIsPlaying] = useState(false);
const [currentTime, setCurrentTime] = useState(0);
const [duration, setDuration] = useState(currentVideo.duration);
const [volume, setVolume] = useState(1);
const [isMuted, setIsMuted] = useState(false);
const [selectedTool, setSelectedTool] = useState<
"select" | "rectangle" | "point"
>("select");
const [selectedLabel, setSelectedLabel] = useState(videoLabels[0]);
const [selectedTrack, setSelectedTrack] = useState<string | null>(null);
const [isDrawing, setIsDrawing] = useState(false);
const [startPoint, setStartPoint] = useState({ x: 0, y: 0 });
const [playbackSpeed, setPlaybackSpeed] = useState(1);
const [isFullscreen, setIsFullscreen] = useState(false);
useEffect(() => {
const video = videoRef.current;
if (!video) return;
const updateTime = () => setCurrentTime(video.currentTime);
const updateDuration = () => setDuration(video.duration);
const handleEnded = () => setIsPlaying(false);
video.addEventListener("timeupdate", updateTime);
video.addEventListener("loadedmetadata", updateDuration);
video.addEventListener("ended", handleEnded);
return () => {
video.removeEventListener("timeupdate", updateTime);
video.removeEventListener("loadedmetadata", updateDuration);
video.removeEventListener("ended", handleEnded);
};
}, []);
useEffect(() => {
drawCanvas();
}, [currentTime, tracks, selectedTrack]);
const drawCanvas = () => {
const canvas = canvasRef.current;
const video = videoRef.current;
if (!canvas || !video) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
// 清空画布
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 绘制当前帧的标注
tracks.forEach((track) => {
if (!track.annotations.length) return;
// 找到当前时间最近的标注
const currentAnnotation = track.annotations
.filter((ann) => Math.abs(ann.frameTime - currentTime) < 0.1)
.sort(
(a, b) =>
Math.abs(a.frameTime - currentTime) -
Math.abs(b.frameTime - currentTime)
)[0];
if (!currentAnnotation || !currentAnnotation.visible) return;
ctx.strokeStyle = track.color;
ctx.fillStyle = track.color + "20";
ctx.lineWidth = selectedTrack === track.id ? 3 : 2;
if (currentAnnotation.type === "rectangle") {
const [x, y, width, height] = currentAnnotation.coordinates;
ctx.strokeRect(x, y, width, height);
ctx.fillRect(x, y, width, height);
// 绘制标签
ctx.fillStyle = track.color;
ctx.fillRect(x, y - 20, ctx.measureText(track.label).width + 8, 20);
ctx.fillStyle = "white";
ctx.font = "12px Arial";
ctx.fillText(track.label, x + 4, y - 6);
} else if (currentAnnotation.type === "point") {
const [x, y] = currentAnnotation.coordinates;
ctx.beginPath();
ctx.arc(x, y, 5, 0, 2 * Math.PI);
ctx.fill();
ctx.stroke();
// 绘制标签
ctx.fillStyle = track.color;
ctx.fillRect(
x + 10,
y - 10,
ctx.measureText(track.label).width + 8,
20
);
ctx.fillStyle = "white";
ctx.font = "12px Arial";
ctx.fillText(track.label, x + 14, y + 4);
}
});
};
const togglePlayPause = () => {
const video = videoRef.current;
if (!video) return;
if (isPlaying) {
video.pause();
} else {
video.play();
}
setIsPlaying(!isPlaying);
};
const handleSeek = (time: number) => {
const video = videoRef.current;
if (!video) return;
video.currentTime = time;
setCurrentTime(time);
};
const handleVolumeChange = (value: number[]) => {
const video = videoRef.current;
if (!video) return;
const newVolume = value[0];
video.volume = newVolume;
setVolume(newVolume);
setIsMuted(newVolume === 0);
};
const toggleMute = () => {
const video = videoRef.current;
if (!video) return;
if (isMuted) {
video.volume = volume;
setIsMuted(false);
} else {
video.volume = 0;
setIsMuted(true);
}
};
const handleSpeedChange = (speed: number) => {
const video = videoRef.current;
if (!video) return;
video.playbackRate = speed;
setPlaybackSpeed(speed);
};
const handleCanvasMouseDown = (e: React.MouseEvent<HTMLCanvasElement>) => {
if (selectedTool === "select") return;
const canvas = canvasRef.current;
if (!canvas) return;
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
if (selectedTool === "point") {
createPointAnnotation(x, y);
} else if (selectedTool === "rectangle") {
setIsDrawing(true);
setStartPoint({ x, y });
}
};
const handleCanvasMouseMove = (e: React.MouseEvent<HTMLCanvasElement>) => {
if (!isDrawing || selectedTool !== "rectangle") return;
const canvas = canvasRef.current;
if (!canvas) return;
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
// 实时预览
drawCanvas();
const ctx = canvas.getContext("2d");
if (!ctx) return;
ctx.strokeStyle = selectedLabel.color;
ctx.lineWidth = 2;
ctx.setLineDash([5, 5]);
ctx.strokeRect(
startPoint.x,
startPoint.y,
x - startPoint.x,
y - startPoint.y
);
ctx.setLineDash([]);
};
const handleCanvasMouseUp = (e: React.MouseEvent<HTMLCanvasElement>) => {
if (!isDrawing || selectedTool !== "rectangle") return;
const canvas = canvasRef.current;
if (!canvas) return;
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const width = x - startPoint.x;
const height = y - startPoint.y;
if (Math.abs(width) > 10 && Math.abs(height) > 10) {
createRectangleAnnotation(startPoint.x, startPoint.y, width, height);
}
setIsDrawing(false);
};
const createPointAnnotation = (x: number, y: number) => {
const newAnnotation: VideoAnnotation = {
id: Date.now().toString(),
frameTime: currentTime,
type: "point",
coordinates: [x, y],
label: selectedLabel.name,
color: selectedLabel.color,
visible: true,
};
const newTrack: VideoTrack = {
id: Date.now().toString(),
label: selectedLabel.name,
color: selectedLabel.color,
annotations: [newAnnotation],
startTime: currentTime,
endTime: currentTime,
};
setTracks([...tracks, newTrack]);
messageApi({
title: "点标注已添加",
description: `在时间 ${formatTime(currentTime)} 添加了点标注`,
});
};
const createRectangleAnnotation = (
x: number,
y: number,
width: number,
height: number
) => {
const newAnnotation: VideoAnnotation = {
id: Date.now().toString(),
frameTime: currentTime,
type: "rectangle",
coordinates: [x, y, width, height],
label: selectedLabel.name,
color: selectedLabel.color,
visible: true,
};
const newTrack: VideoTrack = {
id: Date.now().toString(),
label: selectedLabel.name,
color: selectedLabel.color,
annotations: [newAnnotation],
startTime: currentTime,
endTime: currentTime,
};
setTracks([...tracks, newTrack]);
messageApi.success(`在时间 ${formatTime(currentTime)} 添加了矩形标注`);
};
const deleteTrack = (trackId: string) => {
setTracks(tracks.filter((t) => t.id !== trackId));
setSelectedTrack(null);
messageApi.success("标注轨迹已被删除");
};
const toggleTrackVisibility = (trackId: string) => {
setTracks(
tracks.map((track) =>
track.id === trackId
? {
...track,
annotations: track.annotations.map((ann) => ({
...ann,
visible: !ann.visible,
})),
}
: track
)
);
};
const formatTime = (seconds: number) => {
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins.toString().padStart(2, "0")}:${secs
.toString()
.padStart(2, "0")}`;
};
const toggleFullscreen = () => {
const video = videoRef.current;
if (!video) return;
if (!isFullscreen) {
if (video.requestFullscreen) {
video.requestFullscreen();
}
} else {
if (document.exitFullscreen) {
document.exitFullscreen();
}
}
setIsFullscreen(!isFullscreen);
};
return (
<div className="flex-1 flex">
{/* Tools Panel */}
<div className="w-64 border-r bg-gray-50 p-4 space-y-4">
{/* Tool Selection */}
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-base"></CardTitle>
</CardHeader>
<CardContent className="space-y-2">
<Button
variant={selectedTool === "select" ? "default" : "outline"}
size="sm"
className="w-full justify-start"
onClick={() => setSelectedTool("select")}
>
<MousePointer className="w-4 h-4 mr-2" />
</Button>
<Button
variant={selectedTool === "rectangle" ? "default" : "outline"}
size="sm"
className="w-full justify-start"
onClick={() => setSelectedTool("rectangle")}
>
<Square className="w-4 h-4 mr-2" />
</Button>
<Button
variant={selectedTool === "point" ? "default" : "outline"}
size="sm"
className="w-full justify-start"
onClick={() => setSelectedTool("point")}
>
<Target className="w-4 h-4 mr-2" />
</Button>
</CardContent>
</Card>
{/* Labels */}
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-base"></CardTitle>
</CardHeader>
<CardContent className="space-y-2">
{videoLabels.map((label) => (
<Button
key={label.name}
variant={
selectedLabel.name === label.name ? "default" : "outline"
}
size="sm"
className="w-full justify-start"
onClick={() => setSelectedLabel(label)}
>
<div
className="w-4 h-4 mr-2 rounded"
style={{ backgroundColor: label.color }}
/>
{label.name}
</Button>
))}
</CardContent>
</Card>
{/* Playback Speed */}
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-base"></CardTitle>
</CardHeader>
<CardContent className="space-y-2">
{[0.25, 0.5, 1, 1.5, 2].map((speed) => (
<Button
key={speed}
variant={playbackSpeed === speed ? "default" : "outline"}
size="sm"
className="w-full"
onClick={() => handleSpeedChange(speed)}
>
{speed}x
</Button>
))}
</CardContent>
</Card>
{/* Tracks List */}
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-base"></CardTitle>
</CardHeader>
<CardContent>
<ScrollArea className="h-48">
<div className="space-y-2">
{tracks.map((track) => (
<div
key={track.id}
className={`p-2 border rounded cursor-pointer ${
selectedTrack === track.id
? "border-blue-500 bg-blue-50"
: "border-gray-200"
}`}
onClick={() => setSelectedTrack(track.id)}
>
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<div
className="w-3 h-3 rounded"
style={{ backgroundColor: track.color }}
/>
<span className="text-sm font-medium">
{track.label}
</span>
</div>
<div className="flex items-center space-x-1">
<Button
variant="ghost"
size="sm"
className="p-1 h-auto"
onClick={(e) => {
e.stopPropagation();
toggleTrackVisibility(track.id);
}}
>
{track.annotations[0]?.visible ? (
<Eye className="w-3 h-3" />
) : (
<EyeOff className="w-3 h-3" />
)}
</Button>
<Button
variant="ghost"
size="sm"
className="p-1 h-auto text-red-500"
onClick={(e) => {
e.stopPropagation();
deleteTrack(track.id);
}}
>
<Trash2 className="w-3 h-3" />
</Button>
</div>
</div>
<div className="text-xs text-gray-500 mt-1">
{track.annotations.length}
</div>
</div>
))}
{tracks.length === 0 && (
<div className="text-center py-4 text-gray-500 text-sm">
</div>
)}
</div>
</ScrollArea>
</CardContent>
</Card>
</div>
{/* Video Player and Canvas */}
<div className="flex-1 flex flex-col">
{/* Video Container */}
<div className="flex-1 relative bg-black">
<video
ref={videoRef}
src={currentVideo.url}
className="w-full h-full object-contain"
preload="metadata"
/>
<canvas
ref={canvasRef}
width={800}
height={450}
className="absolute top-0 left-0 w-full h-full cursor-crosshair"
onMouseDown={handleCanvasMouseDown}
onMouseMove={handleCanvasMouseMove}
onMouseUp={handleCanvasMouseUp}
/>
{/* Video Info Overlay */}
<div className="absolute top-4 left-4 bg-black bg-opacity-50 text-white px-3 py-1 rounded text-sm">
{currentVideo.name} | {formatTime(currentTime)} /{" "}
{formatTime(duration)}
</div>
{/* Tool Info Overlay */}
<div className="absolute top-4 right-4 bg-black bg-opacity-50 text-white px-3 py-1 rounded text-sm">
{selectedTool === "select"
? "选择模式"
: selectedTool === "rectangle"
? "矩形标注"
: "点标注"}{" "}
| {selectedLabel.name}
</div>
</div>
{/* Video Controls */}
<div className="border-t bg-white p-4 space-y-4">
{/* Timeline */}
<div className="space-y-2">
<div className="flex items-center justify-between text-sm text-gray-600">
<span>{formatTime(currentTime)}</span>
<span>{formatTime(duration)}</span>
</div>
<Slider
value={[currentTime]}
max={duration}
step={0.1}
onValueChange={(value) => handleSeek(value[0])}
className="w-full"
/>
</div>
{/* Player Controls */}
<div className="flex items-center justify-between">
<div className="flex items-center space-x-4">
<Button
variant="outline"
size="sm"
onClick={() => handleSeek(Math.max(0, currentTime - 10))}
>
<SkipBack className="w-4 h-4" />
</Button>
<Button
onClick={togglePlayPause}
size="lg"
className="bg-blue-600 hover:bg-blue-700"
>
{isPlaying ? (
<Pause className="w-6 h-6" />
) : (
<Play className="w-6 h-6" />
)}
</Button>
<Button
variant="outline"
size="sm"
onClick={() => handleSeek(Math.min(duration, currentTime + 10))}
>
<SkipForward className="w-4 h-4" />
</Button>
{/* Volume Control */}
<div className="flex items-center space-x-2">
<Button variant="ghost" size="sm" onClick={toggleMute}>
{isMuted ? (
<VolumeX className="w-4 h-4" />
) : (
<Volume2 className="w-4 h-4" />
)}
</Button>
<Slider
value={[isMuted ? 0 : volume]}
max={1}
step={0.1}
onValueChange={handleVolumeChange}
className="w-24"
/>
</div>
<Button variant="outline" size="sm" onClick={toggleFullscreen}>
<Maximize className="w-4 h-4" />
</Button>
</div>
<div className="flex items-center space-x-2">
<Badge variant="outline">{playbackSpeed}x</Badge>
<Badge variant="outline">{tracks.length} </Badge>
<Button onClick={onSkipAndNext} variant="outline">
</Button>
<Button
onClick={onSaveAndNext}
className="bg-green-600 hover:bg-green-700"
>
<CheckCircle className="w-4 h-4 mr-2" />
</Button>
</div>
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +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="h-full flex flex-col 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="h-full flex-1 overflow-y-auto flex flex-col 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>
);
}

View File

@@ -0,0 +1,98 @@
import { queryDatasetsUsingGet } from "@/pages/DataManagement/dataset.api";
import { datasetTypeMap } from "@/pages/DataManagement/dataset.const";
import { Button, Form, Input, Modal, Select } from "antd";
import TextArea from "antd/es/input/TextArea";
import { Database } from "lucide-react";
import { useEffect, useState } from "react";
import { createAnnotationTaskUsingPost } from "../../annotation.api";
import { Dataset } from "@/pages/DataManagement/dataset.model";
export default function CreateAnnotationTask({
open,
onClose,
onRefresh,
}: {
open: boolean;
onClose: () => void;
onRefresh: () => void;
}) {
const [form] = Form.useForm();
const [datasets, setDatasets] = useState<Dataset[]>([]);
useEffect(() => {
if (!open) return;
const fetchDatasets = async () => {
const { data } = await queryDatasetsUsingGet({
page: 0,
size: 1000,
});
setDatasets(data.content || []);
};
fetchDatasets();
}, [open]);
const handleSubmit = async () => {
const values = await form.validateFields();
await createAnnotationTaskUsingPost(values);
onClose();
onRefresh();
};
return (
<Modal
open={open}
onCancel={onClose}
title="创建标注任务"
footer={
<>
<Button onClick={onClose}></Button>
<Button type="primary" onClick={handleSubmit}>
</Button>
</>
}
>
<Form layout="vertical">
<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
placeholder="请选择数据集"
options={datasets.map((dataset) => ({
label: (
<div className="flex items-center justify-between gap-3 py-2">
<div className="flex items-center font-sm text-gray-900">
<span>
{dataset.icon || <Database className="w-4 h-4 mr-2" />}
</span>
<span>{dataset.name}</span>
</div>
<div className="text-xs text-gray-500">
{datasetTypeMap[dataset?.datasetType]?.label}
</div>
</div>
),
value: dataset.id,
}))}
/>
</Form.Item>
</Form>
</Modal>
);
}

View File

@@ -0,0 +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>
);
}

View File

@@ -0,0 +1,181 @@
import { useState } from "react";
import { Card, Button, Table, message } from "antd";
import {
PlusOutlined,
EditOutlined,
DeleteOutlined,
SyncOutlined,
} from "@ant-design/icons";
import { SearchControls } from "@/components/SearchControls";
import CardView from "@/components/CardView";
import { useNavigate } from "react-router";
import type { AnnotationTask } from "../annotation.model";
import useFetchData from "@/hooks/useFetchData";
import {
deleteAnnotationTaskByIdUsingDelete,
queryAnnotationTasksUsingGet,
syncAnnotationTaskUsingPost,
} from "../annotation.api";
import { mapAnnotationTask } from "../annotation.const";
import CreateAnnotationTask from "../Create/components/CreateAnnptationTaskDialog";
import { ColumnType } from "antd/es/table";
export default function DataAnnotation() {
const navigate = useNavigate();
const [viewMode, setViewMode] = useState<"list" | "card">("list");
const [showCreateDialog, setShowCreateDialog] = useState(false);
const {
loading,
tableData,
pagination,
searchParams,
setSearchParams,
fetchData,
handleFiltersChange,
} = useFetchData(queryAnnotationTasksUsingGet, mapAnnotationTask);
const handleAnnotate = (task: AnnotationTask) => {
navigate(`/data/annotation/task-annotate/${task.datasetType}/${task.id}`);
};
const handleDelete = async (task: AnnotationTask) => {
await deleteAnnotationTaskByIdUsingDelete({
m: task.id,
proj: task.projId,
});
};
const handleSync = async (task: AnnotationTask, format: string) => {
await syncAnnotationTaskUsingPost({ task, format });
message.success("任务同步请求已发送");
};
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[] = [
{
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: AnnotationTask) => (
<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?.(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>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={() => setShowCreateDialog(true)}
>
</Button>
</div>
{/* Filters Toolbar */}
<SearchControls
searchTerm={searchParams.keyword}
onSearchChange={(keyword) =>
setSearchParams({ ...searchParams, keyword })
}
searchPlaceholder="搜索任务名称、描述"
onFiltersChange={handleFiltersChange}
viewMode={viewMode}
onViewModeChange={setViewMode}
showViewToggle={true}
onReload={fetchData}
/>
{/* Task List/Card */}
{viewMode === "list" ? (
<Card>
<Table
key="id"
loading={loading}
columns={columns}
dataSource={tableData}
pagination={pagination}
scroll={{ x: "max-content", y: "calc(100vh - 20rem)" }}
/>
</Card>
) : (
<CardView data={tableData} operations={operations} />
)}
<CreateAnnotationTask
open={showCreateDialog}
onClose={() => setShowCreateDialog(false)}
onRefresh={fetchData}
/>
</div>
);
}

View File

@@ -0,0 +1,262 @@
import { get, post, put, del, download } from "@/utils/request";
// 标注任务管理相关接口
export function queryAnnotationTasksUsingGet(params?: any) {
return get("/api/project/mappings/list", params);
}
export function createAnnotationTaskUsingPost(data: any) {
return post("/api/project/create", data);
}
export function syncAnnotationTaskUsingPost(data: any) {
return post(`/api/project/sync`, data);
}
export function queryAnnotationTaskByIdUsingGet(taskId: string | number) {
return get(`/api/v1/annotation/tasks/${taskId}`);
}
export function deleteAnnotationTaskByIdUsingDelete(params?: any) {
return del(`/api/project/mappings`, params);
}
// 智能预标注相关接口
export function preAnnotateUsingPost(data: any) {
return post("/api/v1/annotation/pre-annotate", data);
}
// 标注数据管理接口
export function queryAnnotationDataUsingGet(
taskId: string | number,
params?: any
) {
return get(`/api/v1/annotation/tasks/${taskId}/data`, params);
}
export function submitAnnotationUsingPost(taskId: string | number, data: any) {
return post(`/api/v1/annotation/tasks/${taskId}/annotations`, data);
}
export function updateAnnotationUsingPut(
taskId: string | number,
annotationId: string | number,
data: any
) {
return put(
`/api/v1/annotation/tasks/${taskId}/annotations/${annotationId}`,
data
);
}
export function deleteAnnotationUsingDelete(
taskId: string | number,
annotationId: string | number
) {
return del(`/api/v1/annotation/tasks/${taskId}/annotations/${annotationId}`);
}
// 标注任务执行控制
export function startAnnotationTaskUsingPost(taskId: string | number) {
return post(`/api/v1/annotation/tasks/${taskId}/start`);
}
export function pauseAnnotationTaskUsingPost(taskId: string | number) {
return post(`/api/v1/annotation/tasks/${taskId}/pause`);
}
export function resumeAnnotationTaskUsingPost(taskId: string | number) {
return post(`/api/v1/annotation/tasks/${taskId}/resume`);
}
export function completeAnnotationTaskUsingPost(taskId: string | number) {
return post(`/api/v1/annotation/tasks/${taskId}/complete`);
}
// 标注任务统计信息
export function getAnnotationTaskStatisticsUsingGet(taskId: string | number) {
return get(`/api/v1/annotation/tasks/${taskId}/statistics`);
}
export function getAnnotationStatisticsUsingGet(params?: any) {
return get("/api/v1/annotation/statistics", params);
}
// 标注模板管理
export function queryAnnotationTemplatesUsingGet(params?: any) {
return get("/api/v1/annotation/templates", params);
}
export function createAnnotationTemplateUsingPost(data: any) {
return post("/api/v1/annotation/templates", data);
}
export function queryAnnotationTemplateByIdUsingGet(
templateId: string | number
) {
return get(`/api/v1/annotation/templates/${templateId}`);
}
export function updateAnnotationTemplateByIdUsingPut(
templateId: string | number,
data: any
) {
return put(`/api/v1/annotation/templates/${templateId}`, data);
}
export function deleteAnnotationTemplateByIdUsingDelete(
templateId: string | number
) {
return del(`/api/v1/annotation/templates/${templateId}`);
}
// 主动学习相关接口
export function queryActiveLearningCandidatesUsingGet(
taskId: string | number,
params?: any
) {
return get(
`/api/v1/annotation/tasks/${taskId}/active-learning/candidates`,
params
);
}
export function submitActiveLearningFeedbackUsingPost(
taskId: string | number,
data: any
) {
return post(
`/api/v1/annotation/tasks/${taskId}/active-learning/feedback`,
data
);
}
export function updateActiveLearningModelUsingPost(
taskId: string | number,
data: any
) {
return post(
`/api/v1/annotation/tasks/${taskId}/active-learning/update-model`,
data
);
}
// 标注质量控制
export function validateAnnotationsUsingPost(
taskId: string | number,
data: any
) {
return post(`/api/v1/annotation/tasks/${taskId}/validate`, data);
}
export function getAnnotationQualityReportUsingGet(taskId: string | number) {
return get(`/api/v1/annotation/tasks/${taskId}/quality-report`);
}
// 标注数据导入导出
export function exportAnnotationsUsingPost(taskId: string | number, data: any) {
return post(`/api/v1/annotation/tasks/${taskId}/export`, data);
}
export function importAnnotationsUsingPost(taskId: string | number, data: any) {
return post(`/api/v1/annotation/tasks/${taskId}/import`, data);
}
export function downloadAnnotationsUsingGet(
taskId: string | number,
filename?: string
) {
return download(
`/api/v1/annotation/tasks/${taskId}/download`,
null,
filename
);
}
// 标注者管理
export function queryAnnotatorsUsingGet(params?: any) {
return get("/api/v1/annotation/annotators", params);
}
export function assignAnnotatorUsingPost(taskId: string | number, data: any) {
return post(`/api/v1/annotation/tasks/${taskId}/assign`, data);
}
export function getAnnotatorStatisticsUsingGet(annotatorId: string | number) {
return get(`/api/v1/annotation/annotators/${annotatorId}/statistics`);
}
// 标注配置管理
export function getAnnotationConfigUsingGet(taskId: string | number) {
return get(`/api/v1/annotation/tasks/${taskId}/config`);
}
export function updateAnnotationConfigUsingPut(
taskId: string | number,
data: any
) {
return put(`/api/v1/annotation/tasks/${taskId}/config`, data);
}
// 标注类型和标签管理
export function queryAnnotationTypesUsingGet() {
return get("/api/v1/annotation/types");
}
export function queryAnnotationLabelsUsingGet(taskId: string | number) {
return get(`/api/v1/annotation/tasks/${taskId}/labels`);
}
export function createAnnotationLabelUsingPost(
taskId: string | number,
data: any
) {
return post(`/api/v1/annotation/tasks/${taskId}/labels`, data);
}
export function updateAnnotationLabelUsingPut(
taskId: string | number,
labelId: string | number,
data: any
) {
return put(`/api/v1/annotation/tasks/${taskId}/labels/${labelId}`, data);
}
export function deleteAnnotationLabelUsingDelete(
taskId: string | number,
labelId: string | number
) {
return del(`/api/v1/annotation/tasks/${taskId}/labels/${labelId}`);
}
// 批量操作
export function batchAssignAnnotatorsUsingPost(data: any) {
return post("/api/v1/annotation/tasks/batch-assign", data);
}
export function batchUpdateTaskStatusUsingPost(data: any) {
return post("/api/v1/annotation/tasks/batch-update-status", data);
}
export function batchDeleteTasksUsingPost(data: { taskIds: string[] }) {
return post("/api/v1/annotation/tasks/batch-delete", data);
}
// 标注进度跟踪
export function getAnnotationProgressUsingGet(taskId: string | number) {
return get(`/api/v1/annotation/tasks/${taskId}/progress`);
}
// 标注审核
export function submitAnnotationReviewUsingPost(
taskId: string | number,
data: any
) {
return post(`/api/v1/annotation/tasks/${taskId}/review`, data);
}
export function getAnnotationReviewResultsUsingGet(
taskId: string | number,
params?: any
) {
return get(`/api/v1/annotation/tasks/${taskId}/reviews`, params);
}

View File

@@ -0,0 +1,56 @@
import { StickyNote } from "lucide-react";
import { AnnotationTask, AnnotationTaskStatus } from "./annotation.model";
import {
CheckCircleOutlined,
ClockCircleOutlined,
CloseCircleOutlined,
CustomerServiceOutlined,
FileTextOutlined,
PictureOutlined,
VideoCameraOutlined,
} 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: AnnotationTask) {
return {
...task,
id: task.mapping_id,
projId: task.labelling_project_id,
name: task.labelling_project_name,
createdAt: task.created_at,
updatedAt: task.last_updated_at,
icon: <StickyNote />,
iconColor: "bg-blue-100",
status: {
label:
task.status === "completed"
? "已完成"
: task.status === "in_progress"
? "进行中"
: task.status === "skipped"
? "已跳过"
: "待开始",
color: "bg-blue-100",
},
};
}

View File

@@ -0,0 +1,27 @@
import type { DatasetType } from "@/pages/DataManagement/dataset.model";
export enum AnnotationTaskStatus {
ACTIVE = "active",
PROCESSING = "processing",
INACTIVE = "inactive",
}
export interface AnnotationTask {
id: string;
name: string;
annotationCount: number;
createdAt: string;
datasetId: string;
description?: string;
assignedTo?: string;
progress: number;
statistics: {
accuracy: number;
averageTime: number;
reviewCount: number;
};
status: AnnotationTaskStatus;
totalDataCount: number;
type: DatasetType;
updatedAt: string;
}

View File

@@ -0,0 +1,131 @@
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,
},
})),
};
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="h-full mb-4 flex flex-col overflow-auto flex-1 bg-white rounded shadow-sm">
<div className="flex-1 overflow-auto m-6">{renderStepContent()}</div>
<div className="flex justify-end p-6 gap-3 border-t border-gray-200">
<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>
);
}

View File

@@ -0,0 +1,119 @@
import { useState } from "react";
import { Card, Button, Steps, Form, Divider } from "antd";
import { Link, useNavigate } from "react-router";
import { ArrowLeft } from "lucide-react";
import { createCleaningTemplateUsingPost } from "../cleansing.api";
import CleansingTemplateStepOne from "./components/CreateTemplateStepOne";
import { useCreateStepTwo } from "./hooks/useCreateStepTwo";
export default function CleansingTemplateCreate() {
const navigate = useNavigate();
const [form] = Form.useForm();
const [templateConfig, setTemplateConfig] = useState({
name: "",
description: "",
});
const handleSave = async () => {
const template = {
...templateConfig,
instance: selectedOperators.map((item) => ({
id: item.id,
overrides: {
...item.defaultParams,
...item.overrides,
},
})),
};
await createCleaningTemplateUsingPost(template);
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"></h1>
</div>
<div className="w-1/2">
<Steps
size="small"
current={currentStep}
items={[{ title: "基本信息" }, { title: "算子编排" }]}
/>
</div>
</div>
<div className="h-full mb-4 flex flex-col overflow-auto flex-1 bg-white rounded shadow-sm">
<div className="flex-1 overflow-auto m-6">{renderStepContent()}</div>
<div className="flex justify-end p-6 gap-3 border-t border-gray-200">
<Button onClick={() => navigate("/data/cleansing")}></Button>
{currentStep > 1 && <Button onClick={handlePrev}></Button>}
{currentStep === 2 ? (
<Button
type="primary"
onClick={handleSave}
disabled={!canProceed()}
>
</Button>
) : (
<Button
type="primary"
onClick={handleNext}
disabled={!canProceed()}
>
</Button>
)}
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,411 @@
/* PreciseDragDrop.css */
.precise-drag-drop {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
color: #333;
}
.header {
text-align: center;
margin-bottom: 40px;
}
.header h1 {
color: #2c3e50;
margin-bottom: 10px;
font-weight: 600;
font-size: 2rem;
}
.header p {
color: #7f8c8d;
font-size: 1.1rem;
}
.containers {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 30px;
margin-bottom: 40px;
}
.container {
background: white;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
overflow: hidden;
transition: all 0.3s ease;
border: 2px solid transparent;
}
.container.drag-over {
border-color: #3498db;
background-color: #f8fafc;
transform: scale(1.02);
}
.container-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.container-header h2 {
margin: 0;
font-size: 1.4rem;
display: flex;
align-items: center;
gap: 8px;
}
.count {
background: rgba(255, 255, 255, 0.2);
padding: 6px 12px;
border-radius: 20px;
font-size: 0.9rem;
font-weight: 600;
}
.header-actions {
display: flex;
align-items: center;
gap: 10px;
}
.clear-btn {
background: rgba(255, 255, 255, 0.2);
border: 1px solid rgba(255, 255, 255, 0.3);
color: white;
padding: 6px 12px;
border-radius: 6px;
cursor: pointer;
font-size: 0.8rem;
transition: all 0.3s ease;
}
.clear-btn:hover {
background: rgba(255, 255, 255, 0.3);
transform: translateY(-1px);
}
.items-list {
padding: 20px;
min-height: 500px;
max-height: 600px;
overflow-y: auto;
position: relative;
}
.item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px;
margin-bottom: 8px;
background: white;
border-radius: 8px;
border-left: 4px solid var(--item-color);
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08);
transition: all 0.3s ease;
cursor: grab;
position: relative;
}
.item:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.item.dragging {
opacity: 0.6;
cursor: grabbing;
transform: rotate(3deg) scale(1.05);
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.2);
}
.item.drag-over.insert-above {
border-top: 2px dashed var(--item-color);
margin-top: 4px;
}
.item.drag-over.insert-below {
border-bottom: 2px dashed var(--item-color);
margin-bottom: 4px;
}
.item-content {
display: flex;
align-items: center;
gap: 12px;
flex: 1;
}
.item-index {
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
background-color: var(--item-color);
color: white;
border-radius: 50%;
font-size: 0.9rem;
font-weight: bold;
flex-shrink: 0;
}
.item-icon {
font-size: 1.3rem;
flex-shrink: 0;
}
.item-info {
display: flex;
flex-direction: column;
gap: 4px;
flex: 1;
}
.item-title {
font-weight: 500;
font-size: 1rem;
}
.priority-tag {
padding: 2px 8px;
border-radius: 12px;
font-size: 0.7rem;
font-weight: 600;
text-transform: uppercase;
width: fit-content;
}
.priority-high {
background: #ffebee;
color: #c62828;
}
.priority-medium {
background: #fff3e0;
color: #ef6c00;
}
.priority-low {
background: #e8f5e8;
color: #2e7d32;
}
.item-type {
background: #f1f3f4;
padding: 4px 8px;
border-radius: 4px;
font-size: 0.8rem;
color: #666;
text-transform: capitalize;
flex-shrink: 0;
}
.item-actions {
display: flex;
align-items: center;
}
.drag-handle {
color: #bdc3c7;
font-size: 16px;
cursor: grab;
padding: 8px;
user-select: none;
border-radius: 4px;
transition: all 0.3s ease;
}
.drag-handle:hover {
color: #7f8c8d;
background: #f5f5f5;
}
.empty-state {
text-align: center;
padding: 80px 20px;
color: #95a5a6;
}
.empty-state p {
margin: 0 0 8px 0;
font-size: 1.1rem;
font-weight: 500;
}
.empty-state span {
font-size: 0.9rem;
}
/* 插入位置指示器 */
.insert-indicator {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 0;
margin: 4px 0;
opacity: 0.8;
animation: pulse 1.5s infinite;
}
.insert-indicator.above {
margin-bottom: 0;
}
.insert-indicator.below {
margin-top: 0;
}
.indicator-line {
flex: 1;
height: 2px;
background: linear-gradient(90deg, transparent, var(--item-color, #3498db), transparent);
}
.indicator-arrow {
color: var(--item-color, #3498db);
font-weight: bold;
font-size: 0.9rem;
padding: 0 8px;
}
@keyframes pulse {
0%, 100% { opacity: 0.6; }
50% { opacity: 1; }
}
.instructions {
background: #f8f9fa;
padding: 25px;
border-radius: 12px;
border-left: 4px solid #3498db;
}
.instructions h3 {
margin-top: 0;
color: #2c3e50;
text-align: center;
margin-bottom: 20px;
font-size: 1.3rem;
}
.instruction-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
}
.instruction {
display: flex;
align-items: flex-start;
gap: 15px;
padding: 20px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
transition: all 0.3s ease;
}
.instruction:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.instruction .icon {
font-size: 1.8rem;
flex-shrink: 0;
}
.instruction strong {
display: block;
margin-bottom: 5px;
color: #2c3e50;
font-size: 1rem;
}
.instruction p {
margin: 0;
color: #7f8c8d;
font-size: 0.9rem;
line-height: 1.4;
}
/* 动画效果 */
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.item {
animation: slideIn 0.3s ease;
}
/* 滚动条样式 */
.items-list::-webkit-scrollbar {
width: 6px;
}
.items-list::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 3px;
}
.items-list::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 3px;
}
.items-list::-webkit-scrollbar-thumb:hover {
background: #a8a8a8;
}
/* 响应式设计 */
@media (max-width: 768px) {
.containers {
grid-template-columns: 1fr;
gap: 20px;
}
.precise-drag-drop {
padding: 15px;
}
.instruction-grid {
grid-template-columns: 1fr;
}
.container-header {
flex-direction: column;
gap: 10px;
align-items: flex-start;
}
.header-actions {
align-self: flex-end;
}
.item-content {
flex-direction: column;
align-items: flex-start;
gap: 8px;
}
.item-info {
width: 100%;
}
}

View File

@@ -0,0 +1,430 @@
import React, { useState } from "react";
import "./DragDrop.css";
const PreciseDragDrop = () => {
// 初始数据
const [leftItems, setLeftItems] = useState([
{
id: 1,
title: "需求分析",
type: "analysis",
color: "#4CAF50",
priority: "high",
},
{
id: 2,
title: "UI设计",
type: "design",
color: "#2196F3",
priority: "medium",
},
{
id: 3,
title: "前端开发",
type: "development",
color: "#FF9800",
priority: "high",
},
{
id: 4,
title: "后端开发",
type: "development",
color: "#9C27B0",
priority: "high",
},
{
id: 5,
title: "功能测试",
type: "testing",
color: "#3F51B5",
priority: "medium",
},
{
id: 6,
title: "部署上线",
type: "deployment",
color: "#009688",
priority: "low",
},
]);
const [rightItems, setRightItems] = useState([
{
id: 7,
title: "项目启动",
type: "planning",
color: "#E91E63",
priority: "high",
},
]);
const [draggingItem, setDraggingItem] = useState(null);
const [insertPosition, setInsertPosition] = useState(null); // 'above' 或 'below'
// 处理拖拽开始
const handleDragStart = (e, item, source) => {
setDraggingItem({ ...item, source });
e.dataTransfer.effectAllowed = "move";
setTimeout(() => {
e.target.classList.add("dragging");
}, 0);
};
// 处理拖拽结束
const handleDragEnd = (e) => {
setDraggingItem(null);
setInsertPosition(null);
e.target.classList.remove("dragging");
};
// 处理容器拖拽经过
const handleContainerDragOver = (e) => {
e.preventDefault();
};
// 处理容器拖拽离开
const handleContainerDragLeave = (e) => {
if (!e.currentTarget.contains(e.relatedTarget)) {
setInsertPosition(null);
}
};
// 处理项目拖拽经过(用于精确插入)
const handleItemDragOver = (e, itemId) => {
e.preventDefault();
e.stopPropagation();
const rect = e.currentTarget.getBoundingClientRect();
const mouseY = e.clientY;
const elementMiddle = rect.top + rect.height;
// 判断鼠标在元素的上半部分还是下半部分
const newPosition = mouseY < elementMiddle ? "above" : "below";
setInsertPosition(newPosition);
};
// 处理项目拖拽离开
const handleItemDragLeave = (e) => {
if (!e.currentTarget.contains(e.relatedTarget)) {
setInsertPosition(null);
}
};
// 处理放置到右侧容器空白区域
const handleDropToRightContainer = (e) => {
e.preventDefault();
if (!draggingItem) return;
// 如果是从左侧拖拽过来的
if (draggingItem.source === "left") {
// 检查是否已存在
const exists = rightItems.some((item) => item.id === draggingItem.id);
if (!exists) {
setRightItems((prev) => [
...prev,
{
...draggingItem,
source: "right",
},
]);
setLeftItems((prev) =>
prev.filter((item) => item.id !== draggingItem.id)
);
}
}
resetDragState();
};
// 处理放置到右侧容器的特定位置
const handleDropToRightItem = (e, targetItemId) => {
e.preventDefault();
e.stopPropagation();
if (!draggingItem) return;
// 从左侧拖拽到右侧的精确插入
if (draggingItem.source === "left") {
const targetIndex = rightItems.findIndex(
(item) => item.id === targetItemId
);
if (targetIndex !== -1) {
const insertIndex =
insertPosition === "above" ? targetIndex : targetIndex + 1;
// 检查是否已存在
const exists = rightItems.some((item) => item.id === draggingItem.id);
if (!exists) {
const newRightItems = [...rightItems];
newRightItems.splice(insertIndex, 0, {
...draggingItem,
source: "right",
});
setRightItems(newRightItems);
setLeftItems((prev) =>
prev.filter((item) => item.id !== draggingItem.id)
);
}
}
}
// 右侧容器内的重新排序
else if (draggingItem.source === "right") {
const draggedIndex = rightItems.findIndex(
(item) => item.id === draggingItem.id
);
const targetIndex = rightItems.findIndex(
(item) => item.id === targetItemId
);
if (
draggedIndex !== -1 &&
targetIndex !== -1 &&
draggedIndex !== targetIndex
) {
const newItems = [...rightItems];
const [draggedItem] = newItems.splice(draggedIndex, 1);
// 计算正确的插入位置
let insertIndex =
insertPosition === "above" ? targetIndex : targetIndex + 1;
if (draggedIndex < insertIndex) {
insertIndex--; // 调整插入位置,因为已经移除了原元素
}
newItems.splice(insertIndex, 0, draggedItem);
setRightItems(newItems);
}
}
resetDragState();
};
// 处理拖拽回左侧容器
const handleDropToLeft = (e) => {
e.preventDefault();
if (!draggingItem || draggingItem.source !== "right") return;
setRightItems((prev) => prev.filter((item) => item.id !== draggingItem.id));
setLeftItems((prev) => [
...prev,
{
...draggingItem,
source: "left",
},
]);
resetDragState();
};
// 重置拖拽状态
const resetDragState = () => {
setDraggingItem(null);
setInsertPosition(null);
};
// 清空右侧容器
const clearRightContainer = () => {
setLeftItems((prev) => [
...prev,
...rightItems.map((item) => ({
...item,
source: "left",
})),
]);
setRightItems([]);
};
// 获取类型图标
const getTypeIcon = (type) => {
switch (type) {
case "analysis":
return "📊";
case "design":
return "🎨";
case "development":
return "💻";
case "testing":
return "🧪";
case "deployment":
return "🚀";
case "planning":
return "📋";
default:
return "📌";
}
};
// 获取优先级标签
const getPriorityLabel = (priority) => {
switch (priority) {
case "high":
return { label: "高优先级", class: "priority-high" };
case "medium":
return { label: "中优先级", class: "priority-medium" };
case "low":
return { label: "低优先级", class: "priority-low" };
default:
return { label: "普通", class: "priority-medium" };
}
};
return (
<div className="precise-drag-drop">
<div className="header">
<h1></h1>
<p></p>
</div>
<div className="containers">
{/* 左侧容器 - 待办事项 */}
<div
className={`container left-container `}
onDragOver={(e) => handleContainerDragOver(e, "left")}
onDragLeave={handleContainerDragLeave}
onDrop={handleDropToLeft}
>
<div className="container-header">
<h2>📋 </h2>
<span className="count">{leftItems.length} </span>
</div>
<div className="items-list">
{leftItems.map((item) => (
<div
key={item.id}
className="item"
draggable
onDragStart={(e) => handleDragStart(e, item, "left")}
onDragEnd={handleDragEnd}
style={{ "--item-color": item.color }}
>
<div className="item-content">
<span className="item-icon">{getTypeIcon(item.type)}</span>
<div className="item-info">
<span className="item-title">{item.title}</span>
<span
className={`priority-tag ${
getPriorityLabel(item.priority).class
}`}
>
{getPriorityLabel(item.priority).label}
</span>
</div>
</div>
<div className="item-type">{item.type}</div>
</div>
))}
{leftItems.length === 0 && (
<div className="empty-state">
<p>🎉 </p>
<span></span>
</div>
)}
</div>
</div>
{/* 右侧容器 - 进行中的任务 */}
<div
className={`container right-container`}
onDragOver={(e) => handleContainerDragOver(e, "right")}
onDragLeave={handleContainerDragLeave}
onDrop={handleDropToRightContainer}
>
<div className="container-header">
<h2>🚀 </h2>
<div className="header-actions">
<span className="count">{rightItems.length} </span>
{rightItems.length > 0 && (
<button className="clear-btn" onClick={clearRightContainer}>
</button>
)}
</div>
</div>
<div className="items-list">
{rightItems.length === 0 ? (
<div className="empty-state">
<p>📥 </p>
<span></span>
</div>
) : (
rightItems.map((item, index) => (
<div
key={item.id}
className={`item `}
draggable
onDragStart={(e) => handleDragStart(e, item, "right")}
onDragEnd={handleDragEnd}
onDragOver={(e) => handleItemDragOver(e, item.id)}
onDragLeave={handleItemDragLeave}
onDrop={(e) => handleDropToRightItem(e, item.id)}
style={{ "--item-color": item.color }}
>
<div className="item-content">
<span className="item-index">{index + 1}</span>
<span className="item-icon">{getTypeIcon(item.type)}</span>
<div className="item-info">
<span className="item-title">{item.title}</span>
<span
className={`priority-tag ${
getPriorityLabel(item.priority).class
}`}
>
{getPriorityLabel(item.priority).label}
</span>
</div>
</div>
<div className="item-actions">
<span className="drag-handle"></span>
</div>
</div>
))
)}
</div>
</div>
</div>
<div className="instructions">
<h3>🎯 </h3>
<div className="instruction-grid">
<div className="instruction">
<span className="icon">🎯</span>
<div>
<strong></strong>
<p></p>
</div>
</div>
<div className="instruction">
<span className="icon">🔄</span>
<div>
<strong></strong>
<p></p>
</div>
</div>
<div className="instruction">
<span className="icon">📤</span>
<div>
<strong></strong>
<p></p>
</div>
</div>
<div className="instruction">
<span className="icon">🧹</span>
<div>
<strong></strong>
<p>使"清空所有"</p>
</div>
</div>
</div>
</div>
</div>
);
};
export default PreciseDragDrop;

View File

@@ -0,0 +1,119 @@
import RadioCard from "@/components/RadioCard";
import { queryDatasetsUsingGet } from "@/pages/DataManagement/dataset.api";
import {
datasetTypeMap,
datasetTypes,
} 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 { Database } from "lucide-react";
import { useEffect, useMemo, 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: 0, size: 1000 });
setDatasets(data.content || []);
};
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) => ({
label: (
<div className="flex items-center justify-between gap-3 py-2">
<div className="flex items-center font-sm text-gray-900">
<span>
{dataset.icon || <Database className="w-4 h-4 mr-2" />}
</span>
<span>{dataset.name}</span>
</div>
<div className="text-xs text-gray-500">
{datasetTypeMap[dataset?.datasetType]?.label}
</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>
);
}

View File

@@ -0,0 +1,38 @@
import { Input, Form } from "antd";
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 });
};
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>
);
}

View File

@@ -0,0 +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;

View File

@@ -0,0 +1,282 @@
import React, { useMemo, useState } from "react";
import {
Card,
Input,
Select,
Tooltip,
Collapse,
Tag,
Checkbox,
Button,
} from "antd";
import { StarFilled, StarOutlined, SearchOutlined } from "@ant-design/icons";
import { CategoryI, OperatorI } from "@/pages/OperatorMarket/operator.model";
import { Layers } from "lucide-react";
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 OperatorList: React.FC<OperatorListProps> = ({
operators,
favorites,
toggleFavorite,
toggleOperator,
showPoppular,
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>
{showPoppular && operator.isStar && (
<Tag color="gold" className="text-xs">
</Tag>
)}
<span
className="cursor-pointer"
onClick={(e) => {
e.stopPropagation();
toggleFavorite(operator.id);
}}
>
{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(() => {
const filtered = Object.values(groupedOperators).flatMap(
(category) => category.operators
);
return filtered;
}, [groupedOperators]);
// 收藏切换
const toggleFavorite = (operatorId: string) => {
const newFavorites = new Set(favorites);
if (newFavorites.has(operatorId)) {
newFavorites.delete(operatorId);
} else {
newFavorites.add(operatorId);
}
setFavorites(newFavorites);
};
// 全选分类算子
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
showPoppular
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;

View File

@@ -0,0 +1,207 @@
import React, { 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 { OperatorI } from "@/pages/OperatorMarket/operator.model";
interface OperatorFlowProps {
selectedOperators: OperatorI[];
configOperator: OperatorI | null;
templates: CleansingTemplate[];
currentTemplate: CleansingTemplate | null;
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,
setSelectedOperators,
setConfigOperator,
removeOperator,
setCurrentTemplate,
handleDragStart,
handleItemDragLeave,
handleItemDragOver,
handleItemDrop,
handleContainerDragLeave,
handleDropToContainer,
handleDragEnd,
}) => {
const [editingIndex, setEditingIndex] = useState<string | null>(null);
// 添加编号修改处理函数
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-1 overflow-auto p-4 flex flex-col 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"
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>
{/* 分类标签 */}
<Tag color="default"></Tag>
{/* 参数状态指示 */}
{Object.values(operator.configs).some(
(param: any) =>
(param.type === "input" && !param.value) ||
(param.type === "checkbox" &&
Array.isArray(param.value) &&
param.value.length === 0)
) && <Tag color="red"></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 w-10 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;

View File

@@ -0,0 +1,234 @@
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;
const [value, setValue] = React.useState(param.value || param.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 || 0;
const max = param.max || 100;
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={param.step || 1}
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) => (
<Config
key={subParam.key}
operator={operator}
paramKey={subParam.key}
param={subParam}
onParamChange={onParamChange}
/>
))}
</div>
);
default:
return null;
}
};
export default ParamConfig;

View File

@@ -0,0 +1,86 @@
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}
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,
};
}

View File

@@ -0,0 +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,
};
}

View File

@@ -0,0 +1,155 @@
import { useEffect, useState } from "react";
import { OperatorI } from "@/pages/OperatorMarket/operator.model";
import { CleansingTemplate } from "../../cleansing.model";
import { queryCleaningTemplatesUsingGet } from "../../cleansing.api";
import {
queryCategoryTreeUsingGet,
queryOperatorsUsingPost,
} from "@/pages/OperatorMarket/operator.api";
export function useOperatorOperations() {
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 && typeof op.settings === "string"
? 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 () => {
const { data } = await queryCleaningTemplatesUsingGet();
const newTemplates =
data.content?.map?.((item) => ({
...item,
label: item.name,
value: item.id,
})) || [];
setTemplates(newTemplates);
};
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,
};
}

View File

@@ -0,0 +1,176 @@
import { useEffect, useState } from "react";
import { Card, Breadcrumb, App } from "antd";
import {
Play,
Pause,
Clock,
CheckCircle,
AlertCircle,
Database,
Trash2,
Activity,
} from "lucide-react";
import DetailHeader from "@/components/DetailHeader";
import { Link, useNavigate, useParams } from "react-router";
import {
deleteCleaningTaskByIdUsingDelete,
executeCleaningTaskUsingPost,
queryCleaningTaskByIdUsingGet,
stopCleaningTaskUsingPost,
} from "../cleansing.api";
import { TaskStatusMap } from "../cleansing.const";
import { 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";
// 任务详情页面组件
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(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");
};
useEffect(() => {
fetchTaskDetail();
}, [id]);
const [task, setTask] = useState(null);
const [activeTab, setActiveTab] = useState("basic");
const headerData = {
...task,
icon: <Database className="w-8 h-8" />,
status: TaskStatusMap[task?.status],
createdAt: task?.startTime,
lastUpdated: task?.updatedAt,
};
const statistics = [
{
icon: <Clock className="w-4 h-4 text-blue-500" />,
label: "总耗时",
value: task?.duration || "--",
},
{
icon: <CheckCircle className="w-4 h-4 text-green-500" />,
label: "成功文件",
value: task?.successFiles || "--",
},
{
icon: <AlertCircle className="w-4 h-4 text-red-500" />,
label: "失败文件",
value: task?.failedFiles || "--",
},
{
icon: <Activity className="w-4 h-4 text-purple-500" />,
label: "成功率",
value: `${task?.progress}%`,
},
];
const operations = [
...(task?.status === TaskStatus.RUNNING
? [
{
key: "pause",
label: "暂停任务",
icon: <Pause className="w-4 h-4" />,
onClick: pauseTask,
},
]
: []),
...(task?.status === TaskStatus.PENDING
? [
{
key: "start",
label: "执行任务",
icon: <Play className="w-4 h-4" />,
onClick: startTask,
},
]
: []),
{
key: "delete",
label: "删除任务",
icon: <Trash2 className="w-4 h-4" />,
danger: true,
onClick: deleteTask,
},
];
const tabList = [
{
key: "basic",
tab: "基本信息",
children: <BasicInfo task={task} />,
},
{
key: "operators",
tab: "处理算子",
children: <OperatorTable task={task} />,
},
{
key: "files",
tab: "处理文件",
children: <FileTable task={task} />,
},
{ key: "logs", tab: "运行日志", children: <LogsTable task={task} /> },
];
const breadItems = [
{
title: <Link to="/data/cleansing"></Link>,
},
{
title: "清洗任务详情",
},
];
return (
<div className="min-h-screen">
<Breadcrumb items={breadItems} />
<div className="mb-4 mt-4">
<DetailHeader
data={headerData}
statistics={statistics}
operations={operations}
/>
</div>
<Card
tabList={tabList}
activeTabKey={activeTab}
onTabChange={setActiveTab}
></Card>
</div>
);
}

View File

@@ -0,0 +1,148 @@
import type { CleansingTask } from "@/pages/DataCleansing/cleansing.model";
import { OperatorI } from "@/pages/OperatorMarket/operator.model";
import { Button, Card, Descriptions, Progress, Tag } from "antd";
import { Activity, AlertCircle, CheckCircle, Clock } from "lucide-react";
import { useNavigate } from "react-router";
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
type="link"
size="small"
onClick={() =>
navigate("/data/management/detail/" + task?.srcDatasetId)
}
>
{task?.srcDatasetName}
</Button>
),
},
{
key: "targetDataset",
label: "目标数据集",
children: (
<Button
type="link"
size="small"
onClick={() =>
navigate("/data/management/detail/" + task?.destDatasetId)
}
>
{task?.destDatasetName}
</Button>
),
},
{ key: "template", label: "使用模板", children: task?.template },
{ key: "startTime", label: "开始时间", children: task?.startedAt },
{ key: "estimatedTime", label: "预计用时", children: task?.estimatedTime },
{
key: "description",
label: "任务描述",
children: (
<span className="text-gray-600">{task?.description || "暂无描述"}</span>
),
span: 2,
},
{
key: "rules",
label: "处理算子",
children: (
<div className="flex flex-wrap gap-1">
{task?.instance?.map?.((op: OperatorI) => (
<Tag key={op.id}>{op.name}</Tag>
))}
</div>
),
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">
{task?.duration || "--"}
</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?.successFiles || "--"}
</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?.failedFiles || "--"}
</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 || "--"}
</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>
<Progress percent={task?.progress} showInfo />
<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?.processedFiles || "--"}</span>
</div>
<div className="flex items-center gap-2">
<span className="w-3 h-3 bg-blue-500 rounded-full inline-block" />
<span>: {task?.processingFiles || "--"}</span>
</div>
<div className="flex items-center gap-2">
<span className="w-3 h-3 bg-gray-300 rounded-full inline-block" />
<span>
: {task?.totalFiles - task?.processedFiles || "--"}
</span>
</div>
<div className="flex items-center gap-2">
<span className="w-3 h-3 bg-red-500 rounded-full inline-block" />
<span>: {task?.failedFiles || "--"}</span>
</div>
</div>
</div>
</Card>
</>
);
}

View File

@@ -0,0 +1,503 @@
import { Button, Modal, Table, Badge, Input } from "antd";
import { Download, FileText } from "lucide-react";
import { useState } from "react";
// 模拟文件列表数据
const fileList = [
{
id: 1,
fileName: "lung_cancer_001.svs",
originalSize: "15.2MB",
processedSize: "8.5MB",
status: "已完成",
duration: "2分15秒",
processedAt: "2024-01-20 09:32:40",
},
{
id: 2,
fileName: "lung_cancer_002.svs",
originalSize: "18.7MB",
processedSize: "10.2MB",
status: "已完成",
duration: "2分38秒",
processedAt: "2024-01-20 09:35:18",
},
{
id: 3,
fileName: "lung_cancer_003.svs",
originalSize: "12.3MB",
processedSize: "6.8MB",
status: "已完成",
duration: "1分52秒",
processedAt: "2024-01-20 09:37:10",
},
{
id: 4,
fileName: "lung_cancer_004.svs",
originalSize: "20.1MB",
processedSize: "-",
status: "失败",
duration: "0分45秒",
processedAt: "2024-01-20 09:38:55",
},
{
id: 5,
fileName: "lung_cancer_005.svs",
originalSize: "16.8MB",
processedSize: "9.3MB",
status: "已完成",
duration: "2分22秒",
processedAt: "2024-01-20 09:41:17",
},
];
export default function FileTable() {
const [showFileCompareDialog, setShowFileCompareDialog] = useState(false);
const [showFileLogDialog, setShowFileLogDialog] = useState(false);
const [selectedFile, setSelectedFile] = useState<any>(null);
const [selectedFileIds, setSelectedFileIds] = useState<number[]>([]);
const handleSelectAllFiles = (checked: boolean) => {
if (checked) {
setSelectedFileIds(fileList.map((file) => file.id));
} else {
setSelectedFileIds([]);
}
};
const handleSelectFile = (fileId: number, checked: boolean) => {
if (checked) {
setSelectedFileIds([...selectedFileIds, fileId]);
} else {
setSelectedFileIds(selectedFileIds.filter((id) => id !== fileId));
}
};
const handleViewFileCompare = (file: any) => {
setSelectedFile(file);
setShowFileCompareDialog(true);
};
const handleBatchDownload = () => {
// 实际下载逻辑
};
const handleBatchDeleteFiles = () => {
// 实际删除逻辑
setSelectedFileIds([]);
};
const handleViewFileLog = (file: any) => {
setSelectedFile(file);
setShowFileLogDialog(true);
};
// 模拟单个文件的处理日志
const getFileProcessLog = (fileName: string) => [
{
time: "09:30:18",
step: "开始处理",
operator: "格式转换",
status: "INFO",
message: `开始处理文件: ${fileName}`,
},
{
time: "09:30:19",
step: "文件验证",
operator: "格式转换",
status: "INFO",
message: "验证文件格式和完整性",
},
{
time: "09:30:20",
step: "格式解析",
operator: "格式转换",
status: "INFO",
message: "解析SVS格式文件",
},
{
time: "09:30:25",
step: "格式转换",
operator: "格式转换",
status: "SUCCESS",
message: "成功转换为JPEG格式",
},
{
time: "09:30:26",
step: "噪声检测",
operator: "噪声去除",
status: "INFO",
message: "检测图像噪声水平",
},
{
time: "09:30:28",
step: "噪声去除",
operator: "噪声去除",
status: "INFO",
message: "应用高斯滤波去除噪声",
},
{
time: "09:30:31",
step: "噪声去除完成",
operator: "噪声去除",
status: "SUCCESS",
message: "噪声去除处理完成",
},
{
time: "09:30:32",
step: "尺寸检测",
operator: "尺寸标准化",
status: "INFO",
message: "检测当前图像尺寸: 2048x1536",
},
{
time: "09:30:33",
step: "尺寸调整",
operator: "尺寸标准化",
status: "INFO",
message: "调整图像尺寸至512x512",
},
{
time: "09:30:35",
step: "尺寸标准化完成",
operator: "尺寸标准化",
status: "SUCCESS",
message: "图像尺寸标准化完成",
},
{
time: "09:30:36",
step: "质量检查",
operator: "质量检查",
status: "INFO",
message: "检查图像质量指标",
},
{
time: "09:30:38",
step: "分辨率检查",
operator: "质量检查",
status: "SUCCESS",
message: "分辨率符合要求",
},
{
time: "09:30:39",
step: "清晰度检查",
operator: "质量检查",
status: "SUCCESS",
message: "图像清晰度良好",
},
{
time: "09:30:40",
step: "处理完成",
operator: "质量检查",
status: "SUCCESS",
message: `文件 ${fileName} 处理完成`,
},
];
const fileColumns = [
{
title: (
<input
type="checkbox"
checked={
selectedFileIds.length === fileList.length && fileList.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: "fileName",
key: "fileName",
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.fileName.toLowerCase().includes(value.toLowerCase()),
render: (text: string) => (
<span className="font-mono text-sm">{text}</span>
),
},
{
title: "清洗前大小",
dataIndex: "originalSize",
key: "originalSize",
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);
},
},
{
title: "清洗后大小",
dataIndex: "processedSize",
key: "processedSize",
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)
);
},
},
{
title: "状态",
dataIndex: "status",
key: "status",
filters: [
{ text: "已完成", value: "已完成" },
{ text: "失败", value: "失败" },
{ text: "处理中", value: "处理中" },
],
onFilter: (value: string, record: any) => record.status === value,
render: (status: string) => (
<Badge
status={
status === "已完成"
? "success"
: status === "失败"
? "error"
: "processing"
}
text={status}
/>
),
},
{
title: "执行耗时",
dataIndex: "duration",
key: "duration",
sorter: (a: any, b: any) => {
const getTimeInSeconds = (duration: string) => {
const parts = duration.split(/[分秒]/);
const minutes = Number.parseInt(parts[0]) || 0;
const seconds = Number.parseInt(parts[1]) || 0;
return minutes * 60 + seconds;
};
return getTimeInSeconds(a.duration) - getTimeInSeconds(b.duration);
},
},
{
title: "操作",
key: "action",
render: (text: string, record: any) => (
<div className="flex">
<Button
type="link"
size="small"
onClick={() => handleViewFileLog(record)}
>
</Button>
{record.status === "已完成" && (
<Button
type="link"
size="small"
onClick={() => handleViewFileCompare(record)}
>
</Button>
)}
<Button type="link" size="small">
</Button>
</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={fileList}
pagination={{ pageSize: 10, showSizeChanger: true }}
size="middle"
rowKey="id"
/>
{/* 文件日志弹窗 */}
<Modal
open={showFileLogDialog}
onCancel={() => setShowFileLogDialog(false)}
footer={null}
width={700}
title={
<span>
<FileText className="w-4 h-4 mr-2 inline" />
- {selectedFile?.fileName}
</span>
}
>
<div className="py-4">
<div className="bg-gray-900 rounded-lg p-4 max-h-96 overflow-y-auto">
<div className="font-mono text-sm">
{selectedFile &&
getFileProcessLog(selectedFile.fileName).map((log, index) => (
<div key={index} className="flex gap-3">
<span className="text-gray-500 min-w-20">{log.time}</span>
<span className="text-blue-400 min-w-24">
[{log.operator}]
</span>
<span
className={`min-w-20 ${
log.status === "ERROR"
? "text-red-400"
: log.status === "SUCCESS"
? "text-green-400"
: "text-yellow-400"
}`}
>
{log.step}
</span>
<span className="text-gray-100">{log.message}</span>
</div>
))}
</div>
</div>
</div>
</Modal>
{/* 文件对比弹窗 */}
<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">
: {selectedFile?.originalSize}
</div>
</div>
</div>
<div className="text-sm text-gray-600 mt-3 space-y-1">
<div>
<span className="font-medium">:</span> SVS
</div>
<div>
<span className="font-medium">:</span> 2048x1536
</div>
<div>
<span className="font-medium">:</span> RGB
</div>
<div>
<span className="font-medium">:</span>
</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">
: {selectedFile?.processedSize}
</div>
</div>
</div>
<div className="text-sm text-gray-600 mt-3 space-y-1">
<div>
<span className="font-medium">:</span> JPEG
</div>
<div>
<span className="font-medium">:</span> 512x512
</div>
<div>
<span className="font-medium">:</span> RGB
</div>
<div>
<span className="font-medium">:</span> JPEG压缩
</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"> 44.1%</div>
</div>
<div className="bg-blue-50 p-4 rounded-lg">
<div className="font-medium text-blue-700"></div>
<div className="text-blue-600">{selectedFile?.duration}</div>
</div>
<div className="bg-purple-50 p-4 rounded-lg">
<div className="font-medium text-purple-700"></div>
<div className="text-purple-600"> (9.2/10)</div>
</div>
</div>
</div>
</Modal>
</>
);
}

View File

@@ -0,0 +1,110 @@
export default function LogsTable({ task }: { task: any }) {
// 模拟运行日志
const runLogs = [
{
time: "09:30:15",
level: "INFO",
message: "开始执行数据清洗任务: 肺癌WSI图像清洗任务",
},
{
time: "09:30:16",
level: "INFO",
message: "加载源数据集: 肺癌WSI病理图像数据集 (1250 文件)",
},
{ time: "09:30:17", level: "INFO", message: "初始化算子: 格式转换" },
{
time: "09:30:18",
level: "INFO",
message: "开始处理文件: lung_cancer_001.svs",
},
{
time: "09:30:25",
level: "SUCCESS",
message: "文件处理成功: lung_cancer_001.svs -> lung_cancer_001.jpg",
},
{
time: "09:30:26",
level: "INFO",
message: "开始处理文件: lung_cancer_002.svs",
},
{
time: "09:30:33",
level: "SUCCESS",
message: "文件处理成功: lung_cancer_002.svs -> lung_cancer_002.jpg",
},
{
time: "09:58:42",
level: "INFO",
message: "格式转换完成,成功处理 1250/1250 文件",
},
{ time: "09:58:43", level: "INFO", message: "初始化算子: 噪声去除" },
{
time: "09:58:44",
level: "INFO",
message: "开始处理文件: lung_cancer_001.jpg",
},
{
time: "09:58:51",
level: "SUCCESS",
message: "噪声去除成功: lung_cancer_001.jpg",
},
{
time: "10:15:23",
level: "WARNING",
message: "文件质量较低,跳过处理: lung_cancer_156.jpg",
},
{
time: "10:35:18",
level: "INFO",
message: "噪声去除完成,成功处理 1228/1250 文件",
},
{ time: "10:35:19", level: "INFO", message: "初始化算子: 尺寸标准化" },
{
time: "11:12:05",
level: "INFO",
message: "尺寸标准化完成,成功处理 1222/1228 文件",
},
{ time: "11:12:06", level: "INFO", message: "初始化算子: 质量检查" },
{
time: "11:25:33",
level: "ERROR",
message: "质量检查失败: lung_cancer_089.jpg - 分辨率过低",
},
{
time: "11:45:32",
level: "INFO",
message: "质量检查完成,成功处理 1198/1222 文件",
},
{
time: "11:45:33",
level: "SUCCESS",
message: "数据清洗任务完成!总成功率: 95.8%",
},
];
return (
<div className="text-gray-300 p-4 border border-gray-700 bg-gray-800 rounded-lg">
<div className="font-mono text-sm">
{runLogs?.map?.((log, index) => (
<div key={index} className="flex gap-3">
<span className="text-gray-500 min-w-20">{log.time}</span>
<span
className={`min-w-20 ${
log.level === "ERROR"
? "text-red-500"
: log.level === "WARNING"
? "text-yellow-500"
: log.level === "SUCCESS"
? "text-green-500"
: "text-blue-500"
}`}
>
[{log.level}]
</span>
<span className="text-gray-100">{log.message}</span>
</div>
))}
</div>
</div>
);
}

View File

@@ -0,0 +1,103 @@
import { Button, Input, Table } from "antd";
const operators = [
{
name: "格式转换",
startTime: "09:30:15",
endTime: "09:58:42",
duration: "28分27秒",
status: "成功",
processedFiles: 1250,
successRate: 100,
},
{
name: "噪声去除",
startTime: "09:58:42",
endTime: "10:35:18",
duration: "36分36秒",
status: "成功",
processedFiles: 1250,
successRate: 98.2,
},
{
name: "尺寸标准化",
startTime: "10:35:18",
endTime: "11:12:05",
duration: "36分47秒",
status: "成功",
processedFiles: 1228,
successRate: 99.5,
},
{
name: "质量检查",
startTime: "11:12:05",
endTime: "11:45:32",
duration: "33分27秒",
status: "成功",
processedFiles: 1222,
successRate: 97.8,
},
];
export default function OperatorTable({ task }: { task: any }) {
const operatorColumns = [
{
title: "算子名称",
dataIndex: "name",
key: "name",
fixed: "left",
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.name.toLowerCase().includes(value.toLowerCase()),
},
{
title: "版本",
dataIndex: "version",
key: "version",
},
{
title: "创建时间",
dataIndex: "createdAt",
key: "createdAt",
},
{
title: "更新时间",
dataIndex: "updatedAt",
key: "updatedAt",
},
];
return (
<Table
columns={operatorColumns}
dataSource={task?.instance || operators}
pagination={false}
size="middle"
/>
);
}

View File

@@ -0,0 +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>
);
}

View File

@@ -0,0 +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="rounded-xl border border-gray-200 p-6 bg-white">
<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>
);
}

View File

@@ -0,0 +1,257 @@
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,
} = useFetchData(queryCleaningTasksUsingGet, mapTask);
const handleViewTask = (task: any) => {
navigate("/data/cleansing/task-detail/" + task.id);
};
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) => {
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: "删除",
icon: <DeleteOutlined style={{ color: "#f5222d" }} />,
onClick: deleteTask, // implement delete logic
},
];
};
const taskColumns = [
{
title: "任务名称",
dataIndex: "name",
key: "name",
fixed: "left",
width: 150,
},
{
title: "源数据集",
dataIndex: "srcDatasetId",
key: "srcDatasetId",
width: 150,
render: (_, record: CleansingTask) => {
return (
<Button
type="link"
onClick={() =>
navigate("/data/management/detail/" + record.srcDatasetId)
}
>
{record.srcDatasetName}
</Button>
);
},
},
{
title: "目标数据集",
dataIndex: "destDatasetId",
key: "destDatasetId",
width: 150,
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: "startedAt",
key: "startedAt",
width: 180,
},
{
title: "结束时间",
dataIndex: "finishedAt",
key: "finishedAt",
width: 180,
},
{
title: "进度",
dataIndex: "progress",
key: "progress",
width: 200,
render: (progress: number) => (
<Progress percent={progress} size="small" />
),
},
{
title: "创建时间",
dataIndex: "createdAt",
key: "createdAt",
width: 180,
},
{
title: "执行耗时",
dataIndex: "duration",
key: "duration",
width: 180,
},
{
title: "数据量变化",
dataIndex: "dataSizeChange",
key: "dataSizeChange",
width: 180,
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}
onClick={() => op.onClick(record)}
/>
</Tooltip>
) : null
)}
</div>
),
},
];
return (
<>
{/* Search and Filters */}
<SearchControls
searchTerm={searchParams.keyword}
onSearchChange={(keyword) =>
setSearchParams({ ...searchParams, keyword })
}
searchPlaceholder="搜索任务名称、描述"
filters={filterOptions}
onFiltersChange={handleFiltersChange}
viewMode={viewMode}
onViewModeChange={setViewMode}
showViewToggle={true}
onReload={fetchData}
/>
{/* Task List */}
{viewMode === "card" ? (
<CardView
data={tableData}
operations={taskOperations}
pagination={pagination}
/>
) : (
<Card>
<Table
columns={taskColumns}
dataSource={tableData}
rowKey="id"
loading={loading}
scroll={{ x: "max-content", y: "calc(100vh - 35rem)" }}
pagination={pagination}
/>
</Card>
)}
</>
);
}

View File

@@ -0,0 +1,46 @@
import { DeleteOutlined } 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 } from "antd";
import { CleansingTemplate } from "../../cleansing.model";
export default function TemplateList() {
const { message } = App.useApp();
const { tableData, pagination, fetchData } = useFetchData(
queryCleaningTemplatesUsingGet,
mapTemplate
);
const deleteTemplate = async (template: CleansingTemplate) => {
if (!template.id) {
return;
}
// 实现删除逻辑
await deleteCleaningTemplateByIdUsingDelete(template.id);
fetchData();
message.success("模板删除成功");
};
const operations = [
{
key: "delete",
label: "删除模板",
icon: <DeleteOutlined style={{ color: "#f5222d" }} />,
onClick: (template: CleansingTemplate) => deleteTemplate(template), // 可实现删除逻辑
},
];
return (
<CardView
data={tableData}
operations={operations}
pagination={pagination}
/>
);
}

View File

@@ -0,0 +1,57 @@
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 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}`);
}

View File

@@ -0,0 +1,134 @@
import {
CleansingTask,
CleansingTemplate,
TaskStatus,
TemplateType,
} from "@/pages/DataCleansing/cleansing.model";
import {
formatBytes,
formatDateTime,
formatExecutionDuration,
} from "@/utils/unit";
import {
ClockCircleOutlined,
PlayCircleOutlined,
CheckCircleOutlined,
AlertOutlined,
DatabaseOutlined,
AppstoreOutlined,
PauseCircleOutlined,
} from "@ant-design/icons";
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,
createdAt,
startedAt,
finishedAt,
icon: <DatabaseOutlined style={{ color: "#1677ff" }} />,
iconColor: "bg-blue-100",
status,
duration,
before,
after,
statistics: [
{ label: "进度", value: `${task.progress || 0}%` },
{
label: "执行耗时",
value: duration,
},
{
label: "处理前数据大小",
value: task.beforeSize ? formatBytes(task.beforeSize) : "--",
},
{
label: "处理后数据大小",
value: task.afterSize ? formatBytes(task.afterSize) : "--",
},
],
lastModified: formatDateTime(task.createdAt),
};
};
export const mapTemplate = (template: CleansingTemplate) => ({
...template,
createdAt: formatDateTime(template.createdAt),
updatedAt: formatDateTime(template.updatedAt),
icon: <AppstoreOutlined style={{ color: "#1677ff" }} />,
iconColor: "bg-blue-100",
statistics: [{ label: "算子数量", value: template.instance?.length ?? 0 }],
lastModified: formatDateTime(template.updatedAt),
});

View File

@@ -0,0 +1,68 @@
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: number;
operators: 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",
}

View File

@@ -0,0 +1,359 @@
import { useState } from "react";
import {
Card,
Input,
Button,
Select,
Radio,
Form,
Divider,
InputNumber,
TimePicker,
App,
} from "antd";
import { Link, useNavigate } from "react-router";
import { ArrowLeft } from "lucide-react";
import { createTaskUsingPost } from "../collection.apis";
import DevelopmentInProgress from "@/components/DevelopmentInProgress";
const { TextArea } = Input;
interface ScheduleConfig {
type: "immediate" | "scheduled";
scheduleType?: "day" | "week" | "month" | "custom";
time?: string;
dayOfWeek?: string;
dayOfMonth?: string;
cronExpression?: string;
maxRetries?: number;
}
const defaultTemplates = [
{
id: "nas-to-local",
name: "NAS到本地",
description: "从NAS文件系统导入数据到本地文件系统",
config: {
reader: "nasreader",
writer: "localwriter",
},
},
{
id: "obs-to-local",
name: "OBS到本地",
description: "从OBS文件系统导入数据到本地文件系统",
config: {
reader: "obsreader",
writer: "localwriter",
},
},
{
id: "web-tolocal",
name: "Web到本地",
description: "从Web URL导入数据到本地文件系统",
config: {
reader: "webreader",
writer: "localwriter",
},
},
];
export default function CollectionTaskCreate() {
return <DevelopmentInProgress />;
const navigate = useNavigate();
const [form] = Form.useForm();
const { message } = App.useApp();
const [templateType, setTemplateType] = useState<"default" | "custom">(
"default"
);
const [selectedTemplate, setSelectedTemplate] = useState("");
const [customConfig, setCustomConfig] = useState("");
const [scheduleConfig, setScheduleConfig] = useState<ScheduleConfig>({
type: "immediate",
maxRetries: 10,
scheduleType: "daily",
});
const [isCreateDataset, setIsCreateDataset] = useState(false);
const handleSubmit = async () => {
const formData = await form.validateFields();
if (templateType === "default" && !selectedTemplate) {
window.alert("请选择默认模板");
return;
}
if (templateType === "custom" && !customConfig.trim()) {
window.alert("请填写自定义配置");
return;
}
// Create task logic here
const params = {
...formData,
templateType,
selectedTemplate: templateType === "default" ? selectedTemplate : null,
customConfig: templateType === "custom" ? customConfig : null,
scheduleConfig,
};
console.log("Creating task:", params);
await createTaskUsingPost(params);
message.success("任务创建成功");
navigate("/data/collection");
};
return (
<div className="min-h-screen">
<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>
<Card>
<Form
form={form}
layout="vertical"
initialValues={{
name: "",
datasetName: "",
fileFormat: "",
description: "",
cronExpression: "",
retryCount: 3,
timeout: 3600,
incrementalField: "",
}}
onValuesChange={(_, allValues) => {
// 文件格式变化时重置模板选择
if (_.fileFormat !== undefined) setSelectedTemplate("");
}}
>
{/* 基本信息 */}
<h2 className="font-medium text-gray-900 text-lg mb-4"></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>
<Form.Item label="文件格式" name="fileFormat">
<Input placeholder="请填写文件格式,使用正则表达式" />
</Form.Item>
{/* 同步配置 */}
<h2 className="font-medium text-gray-900 my-4 text-lg"></h2>
<Form.Item label="同步方式">
<Radio.Group
value={scheduleConfig.type}
onChange={(e) =>
setScheduleConfig({
type: e.target.value as ScheduleConfig["type"],
})
}
>
<Radio value="immediate"></Radio>
<Radio value="scheduled"></Radio>
</Radio.Group>
</Form.Item>
{scheduleConfig.type === "scheduled" && (
<div className="w-full grid grid-cols-1 md:grid-cols-2 gap-4">
<Form.Item label="调度类型">
<Select
options={[
{ label: "每日", value: "day" },
{ label: "每周", value: "week" },
{ label: "每月", value: "month" },
{ label: "自定义Cron", value: "custom" },
]}
value={scheduleConfig.scheduleType}
onChange={(value) =>
setScheduleConfig((prev) => ({
...prev,
scheduleType: value as ScheduleConfig["scheduleType"],
}))
}
/>
</Form.Item>
{scheduleConfig.scheduleType === "custom" ? (
<Form.Item
label="Cron表达式"
name="cronExpression"
rules={[{ required: true, message: "请输入Cron表达式" }]}
>
<Input
placeholder="例如:0 0 * * * 表示每天午夜执行"
value={scheduleConfig.cronExpression}
onChange={(e) =>
setScheduleConfig((prev) => ({
...prev,
cronExpression: e.target.value,
}))
}
/>
</Form.Item>
) : (
<Form.Item label="执行时间" className="w-full">
{scheduleConfig.scheduleType === "day" ? (
<TimePicker />
) : (
<Select
options={
scheduleConfig.scheduleType === "week"
? [
{ label: "周一", value: "1" },
{ label: "周二", value: "2" },
{ label: "周三", value: "3" },
{ label: "周四", value: "4" },
{ label: "周五", value: "5" },
{ label: "周六", value: "6" },
{ label: "周日", value: "0" },
]
: [
{ label: "每月1日", value: "1" },
{ label: "每月5日", value: "5" },
{ label: "每月10日", value: "10" },
{ label: "每月15日", value: "15" },
{ label: "每月20日", value: "20" },
{ label: "每月25日", value: "25" },
{ label: "每月30日", value: "30" },
]
}
placeholder={
scheduleConfig.scheduleType === "week"
? "选择星期几"
: "选择日期"
}
value={scheduleConfig.dayOfWeek}
onChange={(value) =>
setScheduleConfig((prev) => ({
...prev,
dayOfWeek: value as string,
}))
}
/>
)}
</Form.Item>
)}
</div>
)}
<Form.Item label="最大执行次数">
<InputNumber
min={1}
value={scheduleConfig.maxRetries}
onChange={(value) =>
setScheduleConfig((prev) => ({
...prev,
maxRetries: value,
}))
}
className="w-full"
style={{ width: "100%" }}
/>
</Form.Item>
{/* 模板配置 */}
<h2 className="font-medium text-gray-900 my-4 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)}
>
<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>
)}
{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: "请选择是否创建数据集" }]}
>
<Radio.Group
value={isCreateDataset}
onChange={(e) => setIsCreateDataset(e.target.value)}
>
<Radio value={true}></Radio>
<Radio value={false}></Radio>
</Radio.Group>
</Form.Item>
{isCreateDataset && (
<>
<Form.Item
label="数据集名称"
name="datasetName"
rules={[{ required: true, message: "请输入数据集名称" }]}
>
<Input placeholder="请输入数据集名称" />
</Form.Item>
</>
)}
</>
)}
{/* 提交按钮 */}
<Divider />
<div className="flex gap-2 justify-end">
<Button onClick={() => navigate("/data/collection")}></Button>
<Button type="primary" onClick={handleSubmit}>
</Button>
</div>
</Form>
</Card>
</div>
);
}

View File

@@ -0,0 +1,44 @@
import { useState } from "react";
import { Button, Tabs } from "antd";
import { PlusOutlined } from "@ant-design/icons";
import TaskManagement from "./components/TaskManagement";
import ExecutionLog from "./components/ExecutionLog";
import { useNavigate } from "react-router";
import DevelopmentInProgress from "@/components/DevelopmentInProgress";
export default function DataCollection() {
const navigate = useNavigate();
const [activeTab, setActiveTab] = useState("task-management");
return <DevelopmentInProgress />;
return (
<div>
<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>
);
}

View File

@@ -0,0 +1,153 @@
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,
} = 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={(keyword: string) =>
setSearchParams({
...searchParams,
keyword,
})
}
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>
);
}

View File

@@ -0,0 +1,200 @@
import { Card, Button, Badge, Table, Dropdown, App } from "antd";
import { EllipsisOutlined } 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";
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);
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 columns = [
{
title: "任务名称",
dataIndex: "name",
key: "name",
fixed: "left",
render: (text: string, record: CollectionTask) => (
<Button
type="link"
onClick={() => navigate("`/data-collection/tasks/${record.id}`)}>")}
>
{text}
</Button>
),
},
{
title: "状态",
dataIndex: "status",
key: "status",
render: (status: string) =>
StatusMap[status] ? (
<Badge
color={StatusMap[status].color}
text={StatusMap[status].label}
/>
) : (
<Badge text={status} />
),
},
{
title: "同步方式",
dataIndex: "syncMode",
key: "syncMode",
render: (text: string) => <span>{SyncModeMap[text]?.label}</span>,
},
{
title: "创建时间",
dataIndex: "createdAt",
key: "createdAt",
},
{
title: "更新时间",
dataIndex: "updatedAt",
key: "updatedAt",
},
{
title: "最近执行ID",
dataIndex: "lastExecutionId",
key: "lastExecutionId",
},
{
title: "描述",
dataIndex: "description",
key: "description",
ellipsis: true,
},
{
title: "操作",
key: "action",
fixed: "right" as const,
render: (_: any, record: Task) => (
<Dropdown
menu={{
items: [
record.status === TaskStatus.STOPPED
? {
key: "start",
label: "启动",
onClick: () => handleStartTask(record.id),
}
: {
key: "stop",
label: "停止",
onClick: () => handleStopTask(record.id),
},
{
key: "edit",
label: "编辑",
onClick: () => handleViewDetail(record),
},
{
key: "delete",
label: "删除",
danger: true,
onClick: () => handleDeleteTask(record.id),
},
],
}}
trigger={["click"]}
>
<Button
type="text"
icon={<EllipsisOutlined style={{ fontSize: 20 }} />}
/>
</Dropdown>
),
},
];
return (
<div>
{/* 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: {},
}))
}
className="mb-4"
/>
{/* 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" }}
/>
</Card>
</div>
);
}

View File

@@ -0,0 +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);
}

View File

@@ -0,0 +1,69 @@
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 },
};

View File

@@ -0,0 +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; // 可选,错误信息
}

View File

@@ -0,0 +1,574 @@
import { useState } from "react";
import {
Button,
Card,
Badge,
Input,
Select,
Checkbox,
Form,
Typography,
} from "antd";
import {
PlusOutlined,
ArrowLeftOutlined,
EditOutlined,
SaveOutlined,
DeleteOutlined,
} from "@ant-design/icons";
import {
evaluationTemplates,
presetEvaluationDimensions,
sliceOperators,
} from "@/mock/evaluation";
import { useNavigate } from "react-router";
const { Title, Paragraph } = Typography;
const { Option } = Select;
const EvaluationTaskCreate = () => {
const navigate = useNavigate();
const [datasets, setDatasets] = useState([]);
const [selectedTemplate, setSelectedTemplate] =
useState<string>("dialogue_text");
const [allDimensions, setAllDimensions] = useState<EvaluationDimension[]>([
...presetEvaluationDimensions,
]);
const [editingDimension, setEditingDimension] = useState<string | null>(null);
const [newDimension, setNewDimension] = useState({
name: "",
description: "",
});
const [createForm, setCreateForm] = useState({
name: "",
datasetId: "",
evaluationType: "model" as "model" | "manual",
dimensions: [] as string[],
customDimensions: [] as EvaluationDimension[],
sliceConfig: {
threshold: 0.8,
sampleCount: 100,
method: "语义分割",
},
modelConfig: {
url: "",
apiKey: "",
prompt: "",
temperature: 0.3,
maxTokens: 2000,
},
});
const handleTemplateChange = (templateKey: string) => {
setSelectedTemplate(templateKey);
const template =
evaluationTemplates[templateKey as keyof typeof evaluationTemplates];
if (template) {
const customDimensions = allDimensions.filter((d) => d.isCustom);
setAllDimensions([...template.dimensions, ...customDimensions]);
}
};
const handleAddCustomDimension = () => {
if (newDimension.name.trim() && newDimension.description.trim()) {
const customDimension: EvaluationDimension = {
id: `custom_${Date.now()}`,
name: newDimension.name.trim(),
description: newDimension.description.trim(),
category: "custom",
isCustom: true,
isEnabled: true,
};
setAllDimensions([...allDimensions, customDimension]);
setNewDimension({ name: "", description: "" });
}
};
const handleDimensionToggle = (id: string, checked: boolean) => {
setAllDimensions(
allDimensions.map((d) => (d.id === id ? { ...d, isEnabled: checked } : d))
);
};
const handleEditDimension = (
id: string,
field: "name" | "description",
value: string
) => {
setAllDimensions(
allDimensions.map((d) => (d.id === id ? { ...d, [field]: value } : d))
);
};
const handleDeleteCustomDimension = (id: string) => {
setAllDimensions(allDimensions.filter((d) => d.id !== id));
};
const handleDeletePresetDimension = (id: string) => {
setAllDimensions(
allDimensions.map((d) => (d.id === id ? { ...d, isEnabled: false } : d))
);
};
const handleCreateTask = () => {
const selectedDataset = datasets.find((d) => d.id === createForm.datasetId);
if (!selectedDataset) return;
const enabledDimensions = allDimensions.filter((d) => d.isEnabled);
const presetDimensionIds = enabledDimensions
.filter((d) => !d.isCustom)
.map((d) => d.id);
const customDimensions = enabledDimensions.filter((d) => d.isCustom);
let finalPrompt = createForm.modelConfig.prompt;
if (createForm.evaluationType === "model" && !finalPrompt.trim()) {
finalPrompt = generateDefaultPrompt(selectedDataset.name);
}
const newTask: EvaluationTask = {
id: Date.now().toString(),
name: createForm.name,
datasetId: createForm.datasetId,
datasetName: selectedDataset.name,
evaluationType: createForm.evaluationType,
status: "pending",
progress: 0,
createdAt: new Date().toLocaleString(),
description: `${
createForm.evaluationType === "model" ? "模型自动" : "人工"
}评估${selectedDataset.name}`,
dimensions: presetDimensionIds,
customDimensions: customDimensions,
modelConfig:
createForm.evaluationType === "model"
? {
...createForm.modelConfig,
prompt: finalPrompt,
}
: undefined,
metrics: {
accuracy: 0,
completeness: 0,
consistency: 0,
relevance: 0,
},
issues: [],
};
// 重置表单
setCreateForm({
name: "",
datasetId: "",
evaluationType: "model",
dimensions: [],
customDimensions: [],
modelConfig: {
url: "",
apiKey: "",
prompt: "",
temperature: 0.3,
maxTokens: 2000,
},
});
navigate("/data/evaluation");
};
return (
<div className="h-full">
{/* 页面头部 */}
<div className="flex items-center mb-2">
<Button
type="text"
icon={<ArrowLeftOutlined />}
onClick={() => navigate("/data/evaluation")}
></Button>
<div className="text-xl font-bold"></div>
</div>
<Form layout="vertical">
{/* 基本信息 */}
<Card title="基本信息" style={{ marginBottom: 24 }}>
<Form.Item label="任务名称" required>
<Input
value={createForm.name}
onChange={(e) =>
setCreateForm({ ...createForm, name: e.target.value })
}
placeholder="输入任务名称"
/>
</Form.Item>
<Form.Item label="选择数据集" required>
<Select
value={createForm.datasetId || undefined}
onChange={(value) =>
setCreateForm({ ...createForm, datasetId: value })
}
placeholder="选择要评估的数据集"
>
{datasets.map((dataset) => (
<Option key={dataset.id} value={dataset.id}>
{dataset.name}{dataset.fileCount} {dataset.size}
</Option>
))}
</Select>
</Form.Item>
<Form.Item label="评估方式" required>
<Select
value={createForm.evaluationType}
onChange={(value: "model" | "manual") =>
setCreateForm({ ...createForm, evaluationType: value })
}
>
<Option value="model"></Option>
<Option value="manual"></Option>
</Select>
</Form.Item>
</Card>
{/* 算子配置 */}
<Card title="切片算子配置" style={{ marginBottom: 24 }}>
<Form.Item label="切片算子">
<Select
value={createForm.sliceConfig.method}
onChange={(value) =>
setCreateForm({
...createForm,
sliceConfig: { ...createForm.sliceConfig, method: value },
})
}
placeholder="选择切片算子"
>
{sliceOperators.map((operator) => (
<Option key={operator.id} value={operator.name}>
{operator.name}{" "}
<Badge style={{ marginLeft: 8 }} count={operator.type} />
</Option>
))}
</Select>
</Form.Item>
<Form.Item label="分隔符">
<Input
placeholder="输入分隔符,如 \\n\\n"
value={createForm.sliceConfig.delimiter}
onChange={(e) =>
setCreateForm({
...createForm,
sliceConfig: {
...createForm.sliceConfig,
delimiter: e.target.value,
},
})
}
/>
</Form.Item>
<Form.Item label="分块大小">
<Input
type="number"
value={createForm.sliceConfig.chunkSize}
onChange={(e) =>
setCreateForm({
...createForm,
sliceConfig: {
...createForm.sliceConfig,
chunkSize: Number(e.target.value),
},
})
}
/>
</Form.Item>
<Form.Item label="重叠长度">
<Input
type="number"
value={createForm.sliceConfig.overlapLength}
onChange={(e) =>
setCreateForm({
...createForm,
sliceConfig: {
...createForm.sliceConfig,
overlapLength: Number(e.target.value),
},
})
}
/>
</Form.Item>
<Form.Item label="抽样比例">
<Input
type="number"
value={createForm.sliceConfig.threshold}
onChange={(e) =>
setCreateForm({
...createForm,
sliceConfig: {
...createForm.sliceConfig,
threshold: Number(e.target.value),
},
})
}
/>
</Form.Item>
</Card>
{/* 评估维度配置 */}
<Card
title={
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}
>
<span></span>
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
<Select
value={selectedTemplate}
onChange={handleTemplateChange}
style={{ width: 160 }}
>
{Object.entries(evaluationTemplates).map(
([key, template]) => (
<Option key={key} value={key}>
{template.name}
</Option>
)
)}
</Select>
<Badge
count={allDimensions.filter((d) => d.isEnabled).length}
style={{ background: "#f0f0f0", color: "#333" }}
/>
</div>
</div>
}
style={{ marginBottom: 24 }}
>
{/* 维度表格 */}
<div
style={{
border: "1px solid #f0f0f0",
borderRadius: 6,
marginBottom: 16,
}}
>
<div
style={{
background: "#fafafa",
padding: "8px 12px",
borderBottom: "1px solid #f0f0f0",
fontWeight: 500,
fontSize: 13,
}}
>
<div style={{ display: "flex", alignItems: "center" }}>
<div style={{ width: 60 }}></div>
<div style={{ width: 160 }}></div>
<div style={{ flex: 1 }}></div>
<div style={{ width: 120 }}></div>
</div>
</div>
<div style={{ maxHeight: 320, overflowY: "auto" }}>
{allDimensions.map((dimension) => (
<div
key={dimension.id}
style={{
display: "flex",
alignItems: "center",
padding: "8px 12px",
borderBottom: "1px solid #f5f5f5",
}}
>
<div style={{ width: 60 }}>
<Checkbox
checked={dimension.isEnabled}
onChange={(e) =>
handleDimensionToggle(dimension.id, e.target.checked!)
}
/>
</div>
<div style={{ width: 160 }}>
{editingDimension === dimension.id && dimension.isCustom ? (
<Input
value={dimension.name}
onChange={(e) =>
handleEditDimension(
dimension.id,
"name",
e.target.value
)
}
size="small"
/>
) : (
<span style={{ fontWeight: 500 }}>
{dimension.name}
{dimension.isCustom && (
<Badge
style={{
marginLeft: 4,
background: "#f9f0ff",
color: "#722ed1",
}}
count="自定义"
/>
)}
</span>
)}
</div>
<div style={{ flex: 1 }}>
{editingDimension === dimension.id && dimension.isCustom ? (
<Input
value={dimension.description}
onChange={(e) =>
handleEditDimension(
dimension.id,
"description",
e.target.value
)
}
size="small"
/>
) : (
<span style={{ color: "#888" }}>
{dimension.description}
</span>
)}
</div>
<div style={{ width: 120 }}>
{editingDimension === dimension.id && dimension.isCustom ? (
<Button
type="text"
icon={<SaveOutlined />}
size="small"
onClick={() => setEditingDimension(null)}
/>
) : (
dimension.isCustom && (
<Button
type="text"
icon={<EditOutlined />}
size="small"
onClick={() => setEditingDimension(dimension.id)}
/>
)
)}
<Button
type="text"
icon={<DeleteOutlined />}
size="small"
danger
onClick={() =>
dimension.isCustom
? handleDeleteCustomDimension(dimension.id)
: handleDeletePresetDimension(dimension.id)
}
disabled={
allDimensions.filter((d) => d.isEnabled).length <= 1 &&
dimension.isEnabled
}
/>
</div>
</div>
))}
</div>
</div>
{/* 添加自定义维度 */}
<div style={{ background: "#fafafa", borderRadius: 6, padding: 16 }}>
<div style={{ fontWeight: 500, marginBottom: 8 }}>
</div>
<Input
value={newDimension.name}
onChange={(e) =>
setNewDimension({ ...newDimension, name: e.target.value })
}
placeholder="维度名称"
style={{ width: 180, marginRight: 8 }}
size="small"
/>
<Input
value={newDimension.description}
onChange={(e) =>
setNewDimension({
...newDimension,
description: e.target.value,
})
}
placeholder="维度描述"
style={{ width: 260, marginRight: 8 }}
size="small"
/>
<Button
icon={<PlusOutlined />}
onClick={handleAddCustomDimension}
disabled={
!newDimension.name.trim() || !newDimension.description.trim()
}
size="small"
>
</Button>
</div>
</Card>
{/* 模型配置(仅在选择模型评估时显示) */}
{createForm.evaluationType === "model" && (
<Card title="模型配置" style={{ marginBottom: 24 }}>
<Form.Item label="模型 URL" required>
<Input
value={createForm.modelConfig.url}
onChange={(e) =>
setCreateForm({
...createForm,
modelConfig: {
...createForm.modelConfig,
url: e.target.value,
},
})
}
placeholder="https://api.openai.com/v1/chat/completions"
/>
</Form.Item>
<Form.Item label="API Key" required>
<Input.Password
value={createForm.modelConfig.apiKey}
onChange={(e) =>
setCreateForm({
...createForm,
modelConfig: {
...createForm.modelConfig,
apiKey: e.target.value,
},
})
}
placeholder="sk-***"
/>
</Form.Item>
</Card>
)}
{/* 操作按钮 */}
<Form.Item>
<div style={{ display: "flex", justifyContent: "flex-end", gap: 12 }}>
<Button onClick={() => navigate("/data/evaluation")}></Button>
<Button
type="primary"
onClick={handleCreateTask}
disabled={
!createForm.name ||
!createForm.datasetId ||
allDimensions.filter((d) => d.isEnabled).length === 0 ||
(createForm.evaluationType === "model" &&
(!createForm.modelConfig.url ||
!createForm.modelConfig.apiKey))
}
>
</Button>
</div>
</Form.Item>
</Form>
</div>
);
};
export default EvaluationTaskCreate;

View File

@@ -0,0 +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;

View File

@@ -0,0 +1,484 @@
import { useState, useEffect } from "react";
import { Button, Card, Badge, Progress, Table } from "antd";
import {
PlusOutlined,
DeleteOutlined,
EyeOutlined,
ClockCircleOutlined,
DatabaseOutlined,
CheckCircleOutlined,
CloseCircleOutlined,
ExclamationCircleOutlined,
ReloadOutlined,
UserOutlined,
RobotOutlined,
EditOutlined,
} from "@ant-design/icons";
import { SearchControls } from "@/components/SearchControls";
import { mockTasks } from "@/mock/evaluation";
import CardView from "@/components/CardView";
import { useNavigate } from "react-router";
import type { Dataset } from "@/pages/DataManagement/dataset.model";
import DevelopmentInProgress from "@/components/DevelopmentInProgress";
export default function DataEvaluationPage() {
const navigate = useNavigate();
const [tasks, setTasks] = useState<EvaluationTask[]>(mockTasks);
const [datasets, setDatasets] = useState<Dataset[]>([]);
// 搜索和过滤状态
const [searchTerm, setSearchTerm] = useState("");
const [selectedFilters, setSelectedFilters] = useState<
Record<string, string[]>
>({});
const [sortBy, setSortBy] = useState("createdAt");
const [sortOrder, setSortOrder] = useState<"asc" | "desc">("desc");
const [viewMode, setViewMode] = useState<"card" | "table">("card");
useEffect(() => {
if (typeof window !== "undefined") {
localStorage.setItem("evaluation-tasks", JSON.stringify(tasks));
}
}, [tasks]);
// 搜索和过滤配置
const filterOptions = [
{
key: "evaluationType",
label: "评估方式",
options: [
{ label: "模型评估", value: "model" },
{ label: "人工评估", value: "manual" },
],
},
{
key: "status",
label: "状态",
options: [
{ label: "待处理", value: "pending" },
{ label: "运行中", value: "running" },
{ label: "已完成", value: "completed" },
{ label: "失败", value: "failed" },
],
},
{
key: "dataset",
label: "数据集",
options: datasets.map((d) => ({ label: d.name, value: d.id })),
},
];
const sortOptions = [
{ label: "创建时间", value: "createdAt" },
{ label: "任务名称", value: "name" },
{ label: "完成时间", value: "completedAt" },
{ label: "评分", value: "score" },
];
// 过滤和排序逻辑
const filteredTasks = tasks.filter((task) => {
// 搜索过滤
if (
searchTerm &&
!task.name.toLowerCase().includes(searchTerm.toLowerCase()) &&
!task.datasetName.toLowerCase().includes(searchTerm.toLowerCase())
) {
return false;
}
// 评估方式过滤
if (
selectedFilters.evaluationType?.length &&
!selectedFilters.evaluationType.includes(task.evaluationType)
) {
return false;
}
// 状态过滤
if (
selectedFilters.status?.length &&
!selectedFilters.status.includes(task.status)
) {
return false;
}
// 数据集过滤
if (
selectedFilters.dataset?.length &&
!selectedFilters.dataset.includes(task.datasetId)
) {
return false;
}
return true;
});
// 排序
const sortedTasks = [...filteredTasks].sort((a, b) => {
let aValue: any = a[sortBy as keyof EvaluationTask];
let bValue: any = b[sortBy as keyof EvaluationTask];
if (sortBy === "score") {
aValue = a.score || 0;
bValue = b.score || 0;
}
if (typeof aValue === "string") {
aValue = aValue.toLowerCase();
bValue = bValue.toLowerCase();
}
if (sortOrder === "asc") {
return aValue > bValue ? 1 : -1;
} else {
return aValue < bValue ? 1 : -1;
}
});
const getStatusColor = (status: string) => {
switch (status) {
case "completed":
return "green";
case "running":
return "blue";
case "failed":
return "red";
case "pending":
return "gold";
default:
return "gray";
}
};
const getStatusIcon = (status: string) => {
switch (status) {
case "completed":
return <CheckCircleOutlined />;
case "running":
return <ReloadOutlined spin />;
case "failed":
return <CloseCircleOutlined />;
case "pending":
return <ClockCircleOutlined />;
default:
return <ExclamationCircleOutlined />;
}
};
// 开始人工评估
const handleStartManualEvaluation = (task: EvaluationTask) => {
navigate(`/data/evaluation/manual-evaluate/${task.id}`);
};
// 查看评估报告
const handleViewReport = (task: EvaluationTask) => {
navigate(`/data/evaluation/task-report/${task.id}`);
};
// 删除任务
const handleDeleteTask = (taskId: string) => {
setTasks(tasks.filter((task) => task.id !== taskId));
};
return (
<DevelopmentInProgress />
);
// 主列表界面
return (
<div>
{/* 页面头部 */}
<div className="flex items-center justify-between">
<h1 className="text-xl font-bold"></h1>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={() => navigate("/data/evaluation/create-task")}
>
</Button>
</div>
{/* 搜索和过滤控件 */}
<SearchControls
searchTerm={searchTerm}
onSearchChange={setSearchTerm}
searchPlaceholder="搜索任务名称或数据集..."
filters={filterOptions}
selectedFilters={selectedFilters}
onFiltersChange={setSelectedFilters}
viewMode={viewMode}
onViewModeChange={setViewMode}
/>
{/* 任务列表 */}
{viewMode === "card" ? (
<CardView
data={sortedTasks.map((task) => ({
id: task.id,
name: task.name,
type: task.evaluationType,
icon:
task.evaluationType === "model" ? (
<RobotOutlined style={{ fontSize: 24, color: "#722ed1" }} />
) : (
<UserOutlined style={{ fontSize: 24, color: "#52c41a" }} />
),
iconColor: "",
status: {
label:
task.status === "completed"
? "已完成"
: task.status === "running"
? "运行中"
: task.status === "failed"
? "失败"
: "待处理",
icon: getStatusIcon(task.status),
color: getStatusColor(task.status),
},
description: task.description,
tags: [task.datasetName],
statistics: [
{
label: "进度",
value: task.progress !== undefined ? `${task.progress}%` : "-",
},
{ label: "评分", value: task.score ? `${task.score}` : "-" },
],
lastModified: task.createdAt,
}))}
operations={[
{
key: "view",
label: "查看报告",
icon: <EyeOutlined />,
onClick: (item) => {
const task = tasks.find((t) => t.id === item.id);
if (task) handleViewReport(task);
},
},
{
key: "evaluate",
label: "开始评估",
icon: <EditOutlined />,
onClick: (item) => {
const task = tasks.find((t) => t.id === item.id);
if (task) handleStartManualEvaluation(task);
},
},
{
key: "delete",
label: "删除",
icon: <DeleteOutlined />,
onClick: (item) => handleDeleteTask(item.id as string),
},
]}
onView={(item) => {
const task = tasks.find((t) => t.id === item.id);
if (task) handleViewReport(task);
}}
/>
) : (
<Card>
<Table
rowKey="id"
dataSource={sortedTasks}
pagination={false}
scroll={{ x: "max-content" }}
columns={[
{
title: "任务名称",
dataIndex: "name",
key: "name",
render: (text, record) => (
<div>
<div style={{ fontWeight: 500 }}>{text}</div>
<div style={{ fontSize: 13, color: "#888" }}>
{record.description}
</div>
</div>
),
},
{
title: "数据集",
dataIndex: "datasetName",
key: "datasetName",
render: (text) => (
<div
style={{ display: "flex", alignItems: "center", gap: 4 }}
>
<DatabaseOutlined />
<span style={{ fontSize: 13 }}>{text}</span>
</div>
),
},
{
title: "评估方式",
dataIndex: "evaluationType",
key: "evaluationType",
render: (type) => (
<div
style={{ display: "flex", alignItems: "center", gap: 4 }}
>
{type === "model" ? (
<RobotOutlined style={{ color: "#722ed1" }} />
) : (
<UserOutlined style={{ color: "#52c41a" }} />
)}
<span style={{ fontSize: 13 }}>
{type === "model" ? "模型评估" : "人工评估"}
</span>
</div>
),
},
{
title: "状态",
dataIndex: "status",
key: "status",
render: (status) => (
<Badge
color={getStatusColor(status)}
style={{ background: "none", padding: 0 }}
count={
<span
style={{
display: "flex",
alignItems: "center",
gap: 4,
}}
>
{getStatusIcon(status)}
<span>
{status === "completed" && "已完成"}
{status === "running" && "运行中"}
{status === "failed" && "失败"}
{status === "pending" && "待处理"}
</span>
</span>
}
showZero={false}
/>
),
},
{
title: "进度",
dataIndex: "progress",
key: "progress",
render: (progress) =>
progress !== undefined ? (
<div style={{ width: 100 }}>
<Progress
percent={progress}
size="small"
showInfo={false}
/>
<div
style={{
fontSize: 12,
color: "#888",
textAlign: "right",
}}
>
{progress}%
</div>
</div>
) : (
<span style={{ fontSize: 13, color: "#bbb" }}>-</span>
),
},
{
title: "评分",
dataIndex: "score",
key: "score",
render: (score) =>
score ? (
<span style={{ fontWeight: 500, color: "#389e0d" }}>
{score}
</span>
) : (
<span style={{ fontSize: 13, color: "#bbb" }}>-</span>
),
},
{
title: "创建时间",
dataIndex: "createdAt",
key: "createdAt",
render: (text) => <span style={{ fontSize: 13 }}>{text}</span>,
},
{
title: "操作",
key: "action",
render: (_, task) => (
<div style={{ display: "flex", gap: 8 }}>
{task.status === "completed" && (
<Button
type="default"
size="small"
icon={<EyeOutlined />}
onClick={() => handleViewReport(task)}
>
</Button>
)}
{task.evaluationType === "manual" &&
task.status === "pending" && (
<Button
type="default"
size="small"
icon={<EditOutlined />}
onClick={() => handleStartManualEvaluation(task)}
>
</Button>
)}
<Button
type="text"
size="small"
danger
icon={<DeleteOutlined />}
onClick={() => handleDeleteTask(task.id)}
/>
</div>
),
},
]}
locale={{
emptyText: (
<div
style={{ textAlign: "center", padding: 48, color: "#bbb" }}
>
<DatabaseOutlined style={{ fontSize: 48, marginBottom: 8 }} />
<div style={{ marginTop: 8 }}></div>
<div style={{ fontSize: 13, color: "#ccc" }}>
"创建评估任务"
</div>
</div>
),
}}
/>
</Card>
)}
{sortedTasks.length === 0 && (
<div style={{ textAlign: "center", padding: "48px 0" }}>
<DatabaseOutlined
style={{ fontSize: 64, color: "#bbb", marginBottom: 16 }}
/>
<div style={{ fontSize: 18, fontWeight: 500, marginBottom: 8 }}>
</div>
<div style={{ color: "#888", marginBottom: 24 }}>
</div>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={() => navigate("/data/evaluation/create-task")}
>
</Button>
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,310 @@
import { Button, Card, Badge, Breadcrumb } from "antd";
import {
ArrowLeft,
Download,
Users,
Scissors,
BarChart3,
Target,
Calendar,
TrendingUp,
MessageSquare,
Star,
} from "lucide-react";
import {
mockQAPairs,
mockTasks,
presetEvaluationDimensions,
} from "@/mock/evaluation";
import { Link, useNavigate } 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>
}
bodyStyle={{ 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>
}
bodyStyle={{ 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>
}
bodyStyle={{ 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;

View File

@@ -0,0 +1,243 @@
import { get, post, put, del, download } from "@/utils/request";
// 数据质量评估相关接口
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 createEvaluationTaskUsingPost(data: any) {
return post("/api/v1/evaluation/tasks", data);
}
export function getEvaluationTaskByIdUsingGet(taskId: string | number) {
return get(`/api/v1/evaluation/tasks/${taskId}`);
}
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`);
}

View File

@@ -0,0 +1,73 @@
interface EvaluationDimension {
id: string
name: string
description: string
category: "quality" | "accuracy" | "completeness" | "consistency" | "bias" | "custom"
isCustom?: boolean
isEnabled?: boolean
}
interface EvaluationTask {
id: string
name: string
datasetId: string
datasetName: string
evaluationType: "model" | "manual"
status: "running" | "completed" | "failed" | "pending"
score?: number
progress?: number
createdAt: string
completedAt?: string
description: string
dimensions: string[]
customDimensions: EvaluationDimension[]
sliceConfig?: {
threshold: number
sampleCount: number
method: string
}
modelConfig?: {
url: string
apiKey: string
prompt: string
temperature: number
maxTokens: number
}
metrics: {
accuracy: number
completeness: number
consistency: number
relevance: number
}
issues: {
type: string
count: number
severity: "high" | "medium" | "low"
}[]
}
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
}

View File

@@ -0,0 +1,79 @@
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, DataSource } 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 {
await createDatasetUsingPost(params);
message.success(`数据集创建成功`);
navigate("/data/management");
} 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="h-full flex flex-col flex-1 overflow-auto bg-white border-gray-200 rounded shadow-sm">
<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-t border-gray-200">
<Button onClick={() => navigate("/data/management")}></Button>
<Button type="primary" onClick={handleSubmit}>
</Button>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,101 @@
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, Drawer, Form, Modal } from "antd";
export default function EditDataset({
open,
data,
onClose,
onRefresh,
}: {
open: boolean;
data: Dataset | null;
onClose: () => void;
onRefresh?: () => void;
}) {
const [form] = Form.useForm();
const { message } = App.useApp();
const [newDataset, setNewDataset] = useState({
name: "",
description: "",
datasetType: DatasetType.TEXT,
tags: [],
});
const fetchDataset = async () => {
// 如果有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?.();
} 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>
);
}

View File

@@ -0,0 +1,94 @@
import RadioCard from "@/components/RadioCard";
import { Input, Select, Form } from "antd";
import { datasetTypes } from "../../dataset.const";
import { useEffect, useState } from "react";
import { mockPreparedTags } from "@/components/TagManagement";
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 () => {
try {
const { data } = await queryDatasetTagsUsingGet();
const preparedTags = mockPreparedTags.map((tag) => ({
label: tag.name,
value: tag.name,
}));
const customTags = data.map((tag) => ({
label: tag.name,
value: tag.name,
}));
setTagOptions([
{
label: <span></span>,
title: "prepared",
options: preparedTags,
},
{
label: <span></span>,
title: "custom",
options: 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>
<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>
)}
<Form.Item name="tags" label="标签">
<Select
className="w-full"
mode="tags"
options={tagOptions}
placeholder="请选择标签"
/>
</Form.Item>
</>
);
}

View File

@@ -0,0 +1,227 @@
import { useEffect, useMemo, useState } from "react";
import { Breadcrumb, App, Tabs } from "antd";
import {
ReloadOutlined,
DownloadOutlined,
UploadOutlined,
EditOutlined,
} 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, useParams } from "react-router";
import { useFilesOperation } from "../hooks";
import {
createDatasetTagUsingPost,
downloadFile,
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 [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 handleExportFormat = async ({ type }) => {
await downloadFile(dataset.id, type, `${dataset.name}-${type}.zip`);
message.success("文件下载成功");
};
useEffect(() => {
const refreshDataset = () => {
fetchDataset();
};
const refreshData = () => {
handleRefresh(false);
};
window.addEventListener("update:dataset", refreshData);
window.addEventListener("update:dataset-status", () => refreshDataset());
return () => {
window.removeEventListener("update:dataset", refreshData);
window.removeEventListener("update:dataset-status", refreshDataset);
};
}, []);
// 基本信息描述项
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?.createdAt,
},
];
// 数据集操作列表
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 /> },
// ],
onMenuClick: handleExportFormat,
},
{
key: "refresh",
label: "刷新",
icon: <ReloadOutlined />,
onClick: handleRefresh,
},
];
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="h-full flex flex-col flex-1 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} />
)}
{activeTab === "lineage" && <DataLineageFlow dataset={dataset} />}
{activeTab === "quality" && <DataQuality />}
</div>
</div>
<ImportConfiguration
data={dataset}
open={showUploadDialog}
onClose={() => setShowUploadDialog(false)}
onRefresh={handleRefresh}
/>
<EditDataset
data={dataset}
open={showEditDialog}
onClose={() => setShowEditDialog(false)}
onRefresh={handleRefresh}
/>
</div>
);
}

Some files were not shown because too many files have changed in this diff Show More