Revert "feat: fix the problem in the Operator Market frontend pages" (#204)

Reverts ModelEngine-Group/DataMate#203
This commit is contained in:
Kecheng Sha
2025-12-29 12:01:19 +08:00
committed by GitHub
213 changed files with 45537 additions and 45547 deletions

View File

@@ -1,13 +1,13 @@
root = true root = true
[*] [*]
charset = utf-8 charset = utf-8
end_of_line = lf end_of_line = lf
indent_style = space indent_style = space
indent_size = 2 indent_size = 2
insert_final_newline = true insert_final_newline = true
trim_trailing_whitespace = true trim_trailing_whitespace = true
[*.{md}] [*.{md}]
trim_trailing_whitespace = false trim_trailing_whitespace = false

54
frontend/.gitignore vendored
View File

@@ -1,28 +1,28 @@
# Logs # Logs
logs logs
*.log *.log
npm-debug.log* npm-debug.log*
yarn-debug.log* yarn-debug.log*
yarn-error.log* yarn-error.log*
pnpm-debug.log* pnpm-debug.log*
lerna-debug.log* lerna-debug.log*
node_modules node_modules
dist dist
dist-ssr dist-ssr
*.local *.local
src/mock/sessions/* src/mock/sessions/*
# Editor directories and files # Editor directories and files
.vscode/* .vscode/*
!.vscode/extensions.json !.vscode/extensions.json
.idea .idea
.DS_Store .DS_Store
*.suo *.suo
*.ntvs* *.ntvs*
*.njsproj *.njsproj
*.sln *.sln
*.sw? *.sw?
.vite .vite

View File

@@ -1,96 +1,96 @@
🚀 快速开始 🚀 快速开始
``` ```
npm install # 安装依赖 npm install # 安装依赖
npm run dev # 启动项目 npm run dev # 启动项目
npm run mock # 启动后台Mock服务(可选) npm run mock # 启动后台Mock服务(可选)
``` ```
📁 项目结构 📁 项目结构
``` ```
frontend/ frontend/
├── public/ # 📖 文档中心 ├── public/ # 📖 文档中心
│ ├── huawei-logo.webp/ # logo │ ├── huawei-logo.webp/ # logo
│ └── xxx/ # 标注工作台(可分离部署) │ └── xxx/ # 标注工作台(可分离部署)
├── src/ # 🎨 前端应用 ├── src/ # 🎨 前端应用
│ ├── apps/ # 多前端应用 │ ├── apps/ # 多前端应用
│ │ ├── console/ # 数据工作台&运营控制台 │ │ ├── console/ # 数据工作台&运营控制台
│ │ │ ├── next.config.js │ │ │ ├── next.config.js
│ │ │ ├── package.json │ │ │ ├── package.json
│ │ │ └── src/ │ │ │ └── src/
│ │ └── annotation-studio/ # 标注工作台(可分离部署) │ │ └── annotation-studio/ # 标注工作台(可分离部署)
│ │ │ │
│ ├── assets/ # 共享UI组件/SDK │ ├── assets/ # 共享UI组件/SDK
│ │ ├── xxx/ # 数据工作台&运营控制台 │ │ ├── xxx/ # 数据工作台&运营控制台
│ │ │ ├── next.config.js │ │ │ ├── next.config.js
│ │ │ └── src/ │ │ │ └── src/
│ │ │ │ │ │
│ │ │ │ │ │
│ │ └── xxx/ # 数据工作台&运营控制台 │ │ └── xxx/ # 数据工作台&运营控制台
│ │ ├── package.json │ │ ├── package.json
│ │ └── src/ │ │ └── src/
│ │ │ │
│ ├── components/ # 构建与环境配置 │ ├── components/ # 构建与环境配置
│ │ ├── CardView.tsx # 数据工作台&运营控制台 │ │ ├── CardView.tsx # 数据工作台&运营控制台
│ │ ├── DetailHeader.tsx # 数据工作台&运营控制台 │ │ ├── DetailHeader.tsx # 数据工作台&运营控制台
│ │ ├── RadioCard.tsx # 数据工作台&运营控制台 │ │ ├── RadioCard.tsx # 数据工作台&运营控制台
│ │ ├── SearchControls # 数据工作台&运营控制台 │ │ ├── SearchControls # 数据工作台&运营控制台
│ │ ├── TagList # 标注工作台(可分离部署) │ │ ├── TagList # 标注工作台(可分离部署)
│ │ └── TaskPopover # 标注工作台(可分离部署) │ │ └── TaskPopover # 标注工作台(可分离部署)
│ │ │ │
│ ├── hooks/ # 构建与环境配置 │ ├── hooks/ # 构建与环境配置
│ │ ├── console/ # 数据工作台&运营控制台 │ │ ├── console/ # 数据工作台&运营控制台
│ │ ├── next.config.js │ │ ├── next.config.js
│ │ ├── next.config.js │ │ ├── next.config.js
│ │ ├── next.config.js │ │ ├── next.config.js
│ │ ├── next.config.js │ │ ├── next.config.js
│ │ ├── next.config.js │ │ ├── next.config.js
│ │ └── annotation-studio/ # 标注工作台(可分离部署) │ │ └── annotation-studio/ # 标注工作台(可分离部署)
│ │ │ │
│ ├── mock/ # 构建与环境配置 │ ├── mock/ # 构建与环境配置
│ │ ├── console/ # 数据工作台&运营控制台 │ │ ├── console/ # 数据工作台&运营控制台
│ │ ├── next.config.js │ │ ├── next.config.js
│ │ ├── next.config.js │ │ ├── next.config.js
│ │ ├── next.config.js │ │ ├── next.config.js
│ │ ├── next.config.js │ │ ├── next.config.js
│ │ └── annotation-studio/ # 标注工作台(可分离部署) │ │ └── annotation-studio/ # 标注工作台(可分离部署)
│ │ │ │
│ ├── pages/ # 构建与环境配置 │ ├── pages/ # 构建与环境配置
│ │ ├── console/ # 数据工作台&运营控制台 │ │ ├── console/ # 数据工作台&运营控制台
│ │ │ ├── next.config.js │ │ │ ├── next.config.js
│ │ │ ├── package.json │ │ │ ├── package.json
│ │ │ └── src/ │ │ │ └── src/
│ │ └── annotation-studio/ # 标注工作台(可分离部署) │ │ └── annotation-studio/ # 标注工作台(可分离部署)
│ │ │ │
│ ├── providers/ # 构建与环境配置 │ ├── providers/ # 构建与环境配置
│ │ ├── console/ # 数据工作台&运营控制台 │ │ ├── console/ # 数据工作台&运营控制台
│ │ │ ├── next.config.js │ │ │ ├── next.config.js
│ │ │ ├── package.json │ │ │ ├── package.json
│ │ │ └── src/ │ │ │ └── src/
│ │ └── annotation-studio/ # 标注工作台(可分离部署) │ │ └── annotation-studio/ # 标注工作台(可分离部署)
│ │ │ │
│ ├── routes/ # 构建与环境配置 │ ├── routes/ # 构建与环境配置
│ │ └── next.config.js │ │ └── next.config.js
│ │ │ │
│ ├── types/ # 构建与环境配置 │ ├── types/ # 构建与环境配置
│ │ ├── next.config.js │ │ ├── next.config.js
│ │ ├── next.config.js │ │ ├── next.config.js
│ │ ├── next.config.js │ │ ├── next.config.js
│ │ ├── next.config.js │ │ ├── next.config.js
│ │ └── next.config.js │ │ └── next.config.js
│ │ │ │
│ └── utils/ # 构建与环境配置 │ └── utils/ # 构建与环境配置
│ ├── next.config.js │ ├── next.config.js
│ ├── next.config.js │ ├── next.config.js
│ └── next.config.js │ └── next.config.js
├── eslint.config.js/ # 🔧 后端服务架构 ├── eslint.config.js/ # 🔧 后端服务架构
├── index.html/ # 🔧 后端服务架构 ├── index.html/ # 🔧 后端服务架构
├── package.json/ # 🔧 后端服务架构 ├── package.json/ # 🔧 后端服务架构
├── README.md # 项目说明 ├── README.md # 项目说明
├── tailwind.config.ts # 更新日志 ├── tailwind.config.ts # 更新日志
├── vite.config.ts # 开源协议 ├── vite.config.ts # 开源协议
└── pom.xml # Maven根配置 └── pom.xml # Maven根配置
``` ```

View File

@@ -1,23 +1,23 @@
import js from '@eslint/js' import js from '@eslint/js'
import globals from 'globals' import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks' import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh' import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint' import tseslint from 'typescript-eslint'
import { globalIgnores } from 'eslint/config' import { globalIgnores } from 'eslint/config'
export default tseslint.config([ export default tseslint.config([
globalIgnores(['dist']), globalIgnores(['dist']),
{ {
files: ['**/*.{ts,tsx}'], files: ['**/*.{ts,tsx}'],
extends: [ extends: [
js.configs.recommended, js.configs.recommended,
tseslint.configs.recommended, tseslint.configs.recommended,
reactHooks.configs['recommended-latest'], reactHooks.configs['recommended-latest'],
reactRefresh.configs.vite, reactRefresh.configs.vite,
], ],
languageOptions: { languageOptions: {
ecmaVersion: 2020, ecmaVersion: 2020,
globals: globals.browser, globals: globals.browser,
}, },
}, },
]) ])

View File

@@ -1,13 +1,13 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/huawei-logo.webp" /> <link rel="icon" type="image/svg+xml" href="/huawei-logo.webp" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>DataMate</title> <title>DataMate</title>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
<script type="module" src="/src/main.tsx"></script> <script type="module" src="/src/main.tsx"></script>
</body> </body>
</html> </html>

14450
frontend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,51 +1,51 @@
{ {
"name": "edatamate", "name": "edatamate",
"private": true, "private": true,
"version": "0.0.0", "version": "0.0.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"mock": "cd src/mock && nodemon --config nodemon.json --inspect=0.0.0.0:9229 mock.cjs --env=development --port=8002", "mock": "cd src/mock && nodemon --config nodemon.json --inspect=0.0.0.0:9229 mock.cjs --env=development --port=8002",
"build": "vite build", "build": "vite build",
"lint": "eslint .", "lint": "eslint .",
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@reduxjs/toolkit": "^2.11.0", "@reduxjs/toolkit": "^2.11.0",
"@xyflow/react": "^12.8.3", "@xyflow/react": "^12.8.3",
"antd": "^5.27.0", "antd": "^5.27.0",
"jssha": "^3.3.1", "jssha": "^3.3.1",
"lucide-react": "^0.539.0", "lucide-react": "^0.539.0",
"react": "^18.1.1", "react": "^18.1.1",
"react-dom": "^18.1.1", "react-dom": "^18.1.1",
"react-redux": "^9.2.0", "react-redux": "^9.2.0",
"react-router": "^7.8.0", "react-router": "^7.8.0",
"recharts": "2.15.0" "recharts": "2.15.0"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.33.0", "@eslint/js": "^9.33.0",
"@tailwindcss/vite": "^4.1.12", "@tailwindcss/vite": "^4.1.12",
"@types/node": "^24.2.1", "@types/node": "^24.2.1",
"@types/react": "^18.1.10", "@types/react": "^18.1.10",
"@types/react-dom": "^18.1.7", "@types/react-dom": "^18.1.7",
"@vitejs/plugin-react": "^5.0.0", "@vitejs/plugin-react": "^5.0.0",
"body-parser": "^2.2.0", "body-parser": "^2.2.0",
"eslint": "^9.33.0", "eslint": "^9.33.0",
"eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20", "eslint-plugin-react-refresh": "^0.4.20",
"express": "^5.1.0", "express": "^5.1.0",
"express-session": "^1.18.2", "express-session": "^1.18.2",
"fs-extra": "^11.3.1", "fs-extra": "^11.3.1",
"globals": "^16.3.0", "globals": "^16.3.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"minimist": "^1.2.8", "minimist": "^1.2.8",
"mockjs": "^1.1.0", "mockjs": "^1.1.0",
"nodemon": "^3.1.10", "nodemon": "^3.1.10",
"postcss": "^8.5.6", "postcss": "^8.5.6",
"session-file-store": "^1.5.0", "session-file-store": "^1.5.0",
"tailwindcss": "^4.1.12", "tailwindcss": "^4.1.12",
"typescript": "~5.8.3", "typescript": "~5.8.3",
"typescript-eslint": "^8.39.1", "typescript-eslint": "^8.39.1",
"vite": "^7.1.2" "vite": "^7.1.2"
} }
} }

View File

@@ -1,117 +1,117 @@
import { Dropdown, Popconfirm, Button, Space } from "antd"; import { Dropdown, Popconfirm, Button, Space } from "antd";
import { EllipsisOutlined } from "@ant-design/icons"; import { EllipsisOutlined } from "@ant-design/icons";
import { useState } from "react"; import { useState } from "react";
interface ActionItem { interface ActionItem {
key: string; key: string;
label: string; label: string;
icon?: React.ReactNode; icon?: React.ReactNode;
danger?: boolean; danger?: boolean;
confirm?: { confirm?: {
title: string; title: string;
description?: string; description?: string;
okText?: string; okText?: string;
cancelText?: string; cancelText?: string;
}; };
} }
interface ActionDropdownProps { interface ActionDropdownProps {
actions?: ActionItem[]; actions?: ActionItem[];
onAction?: (key: string, action: ActionItem) => void; onAction?: (key: string, action: ActionItem) => void;
placement?: placement?:
| "bottomRight" | "bottomRight"
| "topLeft" | "topLeft"
| "topCenter" | "topCenter"
| "topRight" | "topRight"
| "bottomLeft" | "bottomLeft"
| "bottomCenter" | "bottomCenter"
| "top" | "top"
| "bottom"; | "bottom";
} }
const ActionDropdown = ({ const ActionDropdown = ({
actions = [], actions = [],
onAction, onAction,
placement = "bottomRight", placement = "bottomRight",
}: ActionDropdownProps) => { }: ActionDropdownProps) => {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const handleActionClick = (action: ActionItem) => { const handleActionClick = (action: ActionItem) => {
if (action.confirm) { if (action.confirm) {
// 如果有确认框,不立即执行,等待确认 // 如果有确认框,不立即执行,等待确认
return; return;
} }
// 执行操作 // 执行操作
onAction?.(action.key, action); onAction?.(action.key, action);
// 如果没有确认框,则立即关闭 Dropdown // 如果没有确认框,则立即关闭 Dropdown
setOpen(false); setOpen(false);
}; };
const dropdownContent = ( const dropdownContent = (
<div className="bg-white p-2 rounded shadow-md"> <div className="bg-white p-2 rounded shadow-md">
<Space direction="vertical" className="w-full"> <Space direction="vertical" className="w-full">
{actions.map((action) => { {actions.map((action) => {
if (action.confirm) { if (action.confirm) {
return ( return (
<Popconfirm <Popconfirm
key={action.key} key={action.key}
title={action.confirm.title} title={action.confirm.title}
description={action.confirm.description} description={action.confirm.description}
onConfirm={() => { onConfirm={() => {
onAction?.(action.key, action); onAction?.(action.key, action);
setOpen(false); setOpen(false);
}} }}
okText={action.confirm.okText || "确定"} okText={action.confirm.okText || "确定"}
cancelText={action.confirm.cancelText || "取消"} cancelText={action.confirm.cancelText || "取消"}
okType={action.danger ? "danger" : "primary"} okType={action.danger ? "danger" : "primary"}
styles={{ root: { zIndex: 9999 } }} styles={{ root: { zIndex: 9999 } }}
> >
<Button <Button
type="text" type="text"
size="small" size="small"
disabled={action.disabled || false} disabled={action.disabled || false}
className="w-full text-left" className="w-full text-left"
danger={action.danger} danger={action.danger}
icon={action.icon} icon={action.icon}
> >
{action.label} {action.label}
</Button> </Button>
</Popconfirm> </Popconfirm>
); );
} }
return ( return (
<Button <Button
key={action.key} key={action.key}
className="w-full" className="w-full"
size="small" size="small"
type="text" type="text"
disabled={action.disabled || false} disabled={action.disabled || false}
danger={action.danger} danger={action.danger}
icon={action.icon} icon={action.icon}
onClick={() => handleActionClick(action)} onClick={() => handleActionClick(action)}
> >
{action.label} {action.label}
</Button> </Button>
); );
})} })}
</Space> </Space>
</div> </div>
); );
return ( return (
<Dropdown <Dropdown
overlay={dropdownContent} overlay={dropdownContent}
trigger={["click"]} trigger={["click"]}
placement={placement} placement={placement}
open={open} open={open}
onOpenChange={setOpen} onOpenChange={setOpen}
> >
<Button <Button
type="text" type="text"
icon={<EllipsisOutlined style={{ fontSize: 24 }} />} icon={<EllipsisOutlined style={{ fontSize: 24 }} />}
/> />
</Dropdown> </Dropdown>
); );
}; };
export default ActionDropdown; export default ActionDropdown;

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,331 +1,331 @@
import React, { useCallback, useEffect } from "react"; import React, { useCallback, useEffect } from "react";
import { Button, Input, Table } from "antd"; import { Button, Input, Table } from "antd";
import { RightOutlined } from "@ant-design/icons"; import { RightOutlined } from "@ant-design/icons";
import { mapDataset } from "@/pages/DataManagement/dataset.const"; import { mapDataset } from "@/pages/DataManagement/dataset.const";
import { import {
Dataset, Dataset,
DatasetFile, DatasetFile,
DatasetType, DatasetType,
} from "@/pages/DataManagement/dataset.model"; } from "@/pages/DataManagement/dataset.model";
import { import {
queryDatasetFilesUsingGet, queryDatasetFilesUsingGet,
queryDatasetsUsingGet, queryDatasetsUsingGet,
} from "@/pages/DataManagement/dataset.api"; } from "@/pages/DataManagement/dataset.api";
import { formatBytes } from "@/utils/unit"; import { formatBytes } from "@/utils/unit";
import { useDebouncedEffect } from "@/hooks/useDebouncedEffect"; import { useDebouncedEffect } from "@/hooks/useDebouncedEffect";
interface DatasetFileTransferProps interface DatasetFileTransferProps
extends React.HTMLAttributes<HTMLDivElement> { extends React.HTMLAttributes<HTMLDivElement> {
open: boolean; open: boolean;
selectedFilesMap: { [key: string]: DatasetFile }; selectedFilesMap: { [key: string]: DatasetFile };
onSelectedFilesChange: (filesMap: { [key: string]: DatasetFile }) => void; onSelectedFilesChange: (filesMap: { [key: string]: DatasetFile }) => void;
onDatasetSelect?: (dataset: Dataset | null) => void; onDatasetSelect?: (dataset: Dataset | null) => void;
} }
const fileCols = [ const fileCols = [
{ {
title: "所属数据集", title: "所属数据集",
dataIndex: "datasetName", dataIndex: "datasetName",
key: "datasetName", key: "datasetName",
ellipsis: true, ellipsis: true,
}, },
{ {
title: "文件名", title: "文件名",
dataIndex: "fileName", dataIndex: "fileName",
key: "fileName", key: "fileName",
ellipsis: true, ellipsis: true,
}, },
{ {
title: "大小", title: "大小",
dataIndex: "fileSize", dataIndex: "fileSize",
key: "fileSize", key: "fileSize",
ellipsis: true, ellipsis: true,
render: formatBytes, render: formatBytes,
}, },
]; ];
// Customize Table Transfer // Customize Table Transfer
const DatasetFileTransfer: React.FC<DatasetFileTransferProps> = ({ const DatasetFileTransfer: React.FC<DatasetFileTransferProps> = ({
open, open,
selectedFilesMap, selectedFilesMap,
onSelectedFilesChange, onSelectedFilesChange,
onDatasetSelect, onDatasetSelect,
...props ...props
}) => { }) => {
const [datasets, setDatasets] = React.useState<Dataset[]>([]); const [datasets, setDatasets] = React.useState<Dataset[]>([]);
const [datasetSearch, setDatasetSearch] = React.useState<string>(""); const [datasetSearch, setDatasetSearch] = React.useState<string>("");
const [datasetPagination, setDatasetPagination] = React.useState<{ const [datasetPagination, setDatasetPagination] = React.useState<{
current: number; current: number;
pageSize: number; pageSize: number;
total: number; total: number;
}>({ current: 1, pageSize: 10, total: 0 }); }>({ current: 1, pageSize: 10, total: 0 });
const [files, setFiles] = React.useState<DatasetFile[]>([]); const [files, setFiles] = React.useState<DatasetFile[]>([]);
const [filesSearch, setFilesSearch] = React.useState<string>(""); const [filesSearch, setFilesSearch] = React.useState<string>("");
const [filesPagination, setFilesPagination] = React.useState<{ const [filesPagination, setFilesPagination] = React.useState<{
current: number; current: number;
pageSize: number; pageSize: number;
total: number; total: number;
}>({ current: 1, pageSize: 10, total: 0 }); }>({ current: 1, pageSize: 10, total: 0 });
const [showFiles, setShowFiles] = React.useState<boolean>(false); const [showFiles, setShowFiles] = React.useState<boolean>(false);
const [selectedDataset, setSelectedDataset] = React.useState<Dataset | null>( const [selectedDataset, setSelectedDataset] = React.useState<Dataset | null>(
null null
); );
const [datasetSelections, setDatasetSelections] = React.useState<Dataset[]>( const [datasetSelections, setDatasetSelections] = React.useState<Dataset[]>(
[] []
); );
const fetchDatasets = async () => { const fetchDatasets = async () => {
const { data } = await queryDatasetsUsingGet({ const { data } = await queryDatasetsUsingGet({
// Ant Design Table pagination.current is 1-based; ensure backend also receives 1-based value // Ant Design Table pagination.current is 1-based; ensure backend also receives 1-based value
page: datasetPagination.current, page: datasetPagination.current,
size: datasetPagination.pageSize, size: datasetPagination.pageSize,
keyword: datasetSearch, keyword: datasetSearch,
type: DatasetType.TEXT, type: DatasetType.TEXT,
}); });
setDatasets(data.content.map(mapDataset) || []); setDatasets(data.content.map(mapDataset) || []);
setDatasetPagination((prev) => ({ setDatasetPagination((prev) => ({
...prev, ...prev,
total: data.totalElements, total: data.totalElements,
})); }));
}; };
useDebouncedEffect( useDebouncedEffect(
() => { () => {
fetchDatasets(); fetchDatasets();
}, },
[datasetSearch, datasetPagination.pageSize, datasetPagination.current], [datasetSearch, datasetPagination.pageSize, datasetPagination.current],
300 300
); );
const fetchFiles = useCallback( const fetchFiles = useCallback(
async ( async (
options?: Partial<{ page: number; pageSize: number; keyword: string }> options?: Partial<{ page: number; pageSize: number; keyword: string }>
) => { ) => {
if (!selectedDataset) return; if (!selectedDataset) return;
const page = options?.page ?? filesPagination.current; const page = options?.page ?? filesPagination.current;
const pageSize = options?.pageSize ?? filesPagination.pageSize; const pageSize = options?.pageSize ?? filesPagination.pageSize;
const keyword = options?.keyword ?? filesSearch; const keyword = options?.keyword ?? filesSearch;
const { data } = await queryDatasetFilesUsingGet(selectedDataset.id, { const { data } = await queryDatasetFilesUsingGet(selectedDataset.id, {
page, page,
size: pageSize, size: pageSize,
keyword, keyword,
}); });
setFiles( setFiles(
(data.content || []).map((item: DatasetFile) => ({ (data.content || []).map((item: DatasetFile) => ({
...item, ...item,
key: item.id, key: item.id,
datasetName: selectedDataset.name, datasetName: selectedDataset.name,
})) }))
); );
setFilesPagination((prev) => ({ setFilesPagination((prev) => ({
...prev, ...prev,
current: page, current: page,
pageSize, pageSize,
total: data.totalElements, total: data.totalElements,
})); }));
}, },
[selectedDataset, filesPagination.current, filesPagination.pageSize, filesSearch] [selectedDataset, filesPagination.current, filesPagination.pageSize, filesSearch]
); );
useEffect(() => { useEffect(() => {
// 当数据集变化时,重置文件分页并拉取第一页文件,避免额外的循环请求 // 当数据集变化时,重置文件分页并拉取第一页文件,避免额外的循环请求
if (selectedDataset) { if (selectedDataset) {
setFilesPagination({ current: 1, pageSize: 10, total: 0 }); setFilesPagination({ current: 1, pageSize: 10, total: 0 });
fetchFiles({ page: 1, pageSize: 10 }).catch(() => {}); fetchFiles({ page: 1, pageSize: 10 }).catch(() => {});
} else { } else {
setFiles([]); setFiles([]);
setFilesPagination({ current: 1, pageSize: 10, total: 0 }); setFilesPagination({ current: 1, pageSize: 10, total: 0 });
} }
// 只在 selectedDataset 变化时触发 // 只在 selectedDataset 变化时触发
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedDataset]); }, [selectedDataset]);
useEffect(() => { useEffect(() => {
onDatasetSelect?.(selectedDataset); onDatasetSelect?.(selectedDataset);
}, [selectedDataset, onDatasetSelect]); }, [selectedDataset, onDatasetSelect]);
const toggleSelectFile = (record: DatasetFile) => { const toggleSelectFile = (record: DatasetFile) => {
if (!selectedFilesMap[record.id]) { if (!selectedFilesMap[record.id]) {
onSelectedFilesChange({ onSelectedFilesChange({
...selectedFilesMap, ...selectedFilesMap,
[record.id]: record, [record.id]: record,
}); });
} else { } else {
const newSelectedFiles = { ...selectedFilesMap }; const newSelectedFiles = { ...selectedFilesMap };
delete newSelectedFiles[record.id]; delete newSelectedFiles[record.id];
onSelectedFilesChange(newSelectedFiles); onSelectedFilesChange(newSelectedFiles);
} }
}; };
useEffect(() => { useEffect(() => {
if (!open) { if (!open) {
// 重置状态 // 重置状态
setDatasets([]); setDatasets([]);
setDatasetSearch(""); setDatasetSearch("");
setDatasetPagination({ current: 1, pageSize: 10, total: 0 }); setDatasetPagination({ current: 1, pageSize: 10, total: 0 });
setFiles([]); setFiles([]);
setFilesSearch(""); setFilesSearch("");
setFilesPagination({ current: 1, pageSize: 10, total: 0 }); setFilesPagination({ current: 1, pageSize: 10, total: 0 });
setShowFiles(false); setShowFiles(false);
setSelectedDataset(null); setSelectedDataset(null);
setDatasetSelections([]); setDatasetSelections([]);
onDatasetSelect?.(null); onDatasetSelect?.(null);
} }
}, [open, onDatasetSelect]); }, [open, onDatasetSelect]);
const datasetCols = [ const datasetCols = [
{ {
title: "数据集名称", title: "数据集名称",
dataIndex: "name", dataIndex: "name",
key: "name", key: "name",
ellipsis: true, ellipsis: true,
}, },
{ {
title: "文件数", title: "文件数",
dataIndex: "fileCount", dataIndex: "fileCount",
key: "fileCount", key: "fileCount",
ellipsis: true, ellipsis: true,
}, },
{ {
title: "大小", title: "大小",
dataIndex: "totalSize", dataIndex: "totalSize",
key: "totalSize", key: "totalSize",
ellipsis: true, ellipsis: true,
render: formatBytes, render: formatBytes,
}, },
]; ];
return ( return (
<div {...props}> <div {...props}>
<div className="grid grid-cols-25 gap-4 w-full"> <div className="grid grid-cols-25 gap-4 w-full">
<div className="border-card flex flex-col col-span-12"> <div className="border-card flex flex-col col-span-12">
<div className="border-bottom p-2 font-bold"></div> <div className="border-bottom p-2 font-bold"></div>
<div className="p-2"> <div className="p-2">
<Input <Input
placeholder="搜索数据集名称..." placeholder="搜索数据集名称..."
value={datasetSearch} value={datasetSearch}
allowClear allowClear
onChange={(e) => setDatasetSearch(e.target.value)} onChange={(e) => setDatasetSearch(e.target.value)}
/> />
</div> </div>
<Table <Table
scroll={{ y: 400 }} scroll={{ y: 400 }}
rowKey="id" rowKey="id"
size="small" size="small"
rowClassName={(record) => rowClassName={(record) =>
selectedDataset?.id === record.id ? "bg-blue-100" : "" selectedDataset?.id === record.id ? "bg-blue-100" : ""
} }
onRow={(record: Dataset) => ({ onRow={(record: Dataset) => ({
onClick: () => { onClick: () => {
setSelectedDataset(record); setSelectedDataset(record);
if (!datasetSelections.find((d) => d.id === record.id)) { if (!datasetSelections.find((d) => d.id === record.id)) {
setDatasetSelections([...datasetSelections, record]); setDatasetSelections([...datasetSelections, record]);
} else { } else {
setDatasetSelections( setDatasetSelections(
datasetSelections.filter((d) => d.id !== record.id) datasetSelections.filter((d) => d.id !== record.id)
); );
} }
}, },
})} })}
dataSource={datasets} dataSource={datasets}
columns={datasetCols} columns={datasetCols}
pagination={{ pagination={{
...datasetPagination, ...datasetPagination,
onChange: (page, pageSize) => onChange: (page, pageSize) =>
setDatasetPagination({ setDatasetPagination({
current: page, current: page,
pageSize: pageSize || datasetPagination.pageSize, pageSize: pageSize || datasetPagination.pageSize,
total: datasetPagination.total, total: datasetPagination.total,
}), }),
}} }}
/> />
</div> </div>
<RightOutlined /> <RightOutlined />
<div className="border-card flex flex-col col-span-12"> <div className="border-card flex flex-col col-span-12">
<div className="border-bottom p-2 font-bold"></div> <div className="border-bottom p-2 font-bold"></div>
<div className="p-2"> <div className="p-2">
<Input <Input
placeholder="搜索文件名称..." placeholder="搜索文件名称..."
value={filesSearch} value={filesSearch}
onChange={(e) => setFilesSearch(e.target.value)} onChange={(e) => setFilesSearch(e.target.value)}
/> />
</div> </div>
<Table <Table
scroll={{ y: 400 }} scroll={{ y: 400 }}
rowKey="id" rowKey="id"
size="small" size="small"
dataSource={files} dataSource={files}
columns={fileCols.slice(1, fileCols.length)} columns={fileCols.slice(1, fileCols.length)}
pagination={{ pagination={{
...filesPagination, ...filesPagination,
onChange: (page, pageSize) => { onChange: (page, pageSize) => {
const nextPageSize = pageSize || filesPagination.pageSize; const nextPageSize = pageSize || filesPagination.pageSize;
setFilesPagination((prev) => ({ setFilesPagination((prev) => ({
...prev, ...prev,
current: page, current: page,
pageSize: nextPageSize, pageSize: nextPageSize,
})); }));
fetchFiles({ page, pageSize: nextPageSize }).catch(() => {}); fetchFiles({ page, pageSize: nextPageSize }).catch(() => {});
}, },
}} }}
onRow={(record: DatasetFile) => ({ onRow={(record: DatasetFile) => ({
onClick: () => toggleSelectFile(record), onClick: () => toggleSelectFile(record),
})} })}
rowSelection={{ rowSelection={{
type: "checkbox", type: "checkbox",
selectedRowKeys: Object.keys(selectedFilesMap), selectedRowKeys: Object.keys(selectedFilesMap),
// 单选 // 单选
onSelect: (record: DatasetFile) => { onSelect: (record: DatasetFile) => {
toggleSelectFile(record); toggleSelectFile(record);
}, },
// 全选 // 全选
onSelectAll: (selected, selectedRows: DatasetFile[]) => { onSelectAll: (selected, selectedRows: DatasetFile[]) => {
if (selected) { if (selected) {
// ✔ 全选 -> 将 files 列表全部加入 selectedFilesMap // ✔ 全选 -> 将 files 列表全部加入 selectedFilesMap
const newMap: Record<string, DatasetFile> = { ...selectedFilesMap }; const newMap: Record<string, DatasetFile> = { ...selectedFilesMap };
selectedRows.forEach((f) => { selectedRows.forEach((f) => {
newMap[f.id] = f; newMap[f.id] = f;
}); });
onSelectedFilesChange(newMap); onSelectedFilesChange(newMap);
} else { } else {
// ✘ 取消全选 -> 清空 map // ✘ 取消全选 -> 清空 map
const newMap = { ...selectedFilesMap }; const newMap = { ...selectedFilesMap };
Object.keys(newMap).forEach((id) => { Object.keys(newMap).forEach((id) => {
if (files.some((f) => String(f.id) === id)) { if (files.some((f) => String(f.id) === id)) {
// 仅移除当前页对应文件 // 仅移除当前页对应文件
delete newMap[id]; delete newMap[id];
} }
}); });
onSelectedFilesChange(newMap); onSelectedFilesChange(newMap);
} }
}, },
getCheckboxProps: (record: DatasetFile) => ({ getCheckboxProps: (record: DatasetFile) => ({
name: record.fileName, name: record.fileName,
}), }),
}} }}
/> />
</div> </div>
</div> </div>
<Button className="mt-4" onClick={() => setShowFiles(!showFiles)}> <Button className="mt-4" onClick={() => setShowFiles(!showFiles)}>
{showFiles ? "取消预览" : "预览"} {showFiles ? "取消预览" : "预览"}
</Button> </Button>
<div hidden={!showFiles}> <div hidden={!showFiles}>
<Table <Table
scroll={{ y: 400 }} scroll={{ y: 400 }}
rowKey="id" rowKey="id"
size="small" size="small"
dataSource={Object.values(selectedFilesMap)} dataSource={Object.values(selectedFilesMap)}
columns={fileCols} columns={fileCols}
/> />
</div> </div>
</div> </div>
); );
}; };
export default DatasetFileTransfer; export default DatasetFileTransfer;

View File

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

View File

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

View File

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

View File

@@ -1,238 +1,236 @@
// 首页数据获取 // 首页数据获取
// 支持轮询功能,使用示例: // 支持轮询功能,使用示例:
// const { fetchData, startPolling, stopPolling, isPolling } = useFetchData( // const { fetchData, startPolling, stopPolling, isPolling } = useFetchData(
// fetchFunction, // fetchFunction,
// mapFunction, // mapFunction,
// 5000, // 5秒轮询一次,默认30秒 // 5000, // 5秒轮询一次,默认30秒
// true, // 是否自动开始轮询,默认 true // true, // 是否自动开始轮询,默认 true
// [fetchStatistics, fetchOtherData] // 额外的轮询函数数组 // [fetchStatistics, fetchOtherData] // 额外的轮询函数数组
// ); // );
// //
// startPolling(); // 开始轮询 // startPolling(); // 开始轮询
// stopPolling(); // 停止轮询 // stopPolling(); // 停止轮询
// 手动调用 fetchData() 时,如果正在轮询,会重新开始轮询计时 // 手动调用 fetchData() 时,如果正在轮询,会重新开始轮询计时
// 轮询时会同时执行主要的 fetchFunction 和所有额外的轮询函数 // 轮询时会同时执行主要的 fetchFunction 和所有额外的轮询函数
import { useState, useRef, useEffect, useCallback } from "react"; import { useState, useRef, useEffect, useCallback } from "react";
import { useDebouncedEffect } from "./useDebouncedEffect"; import { useDebouncedEffect } from "./useDebouncedEffect";
import Loading from "@/utils/loading"; import Loading from "@/utils/loading";
import { App } from "antd"; import { App } from "antd";
export default function useFetchData<T>( export default function useFetchData<T>(
fetchFunc: (params?: any) => Promise<any>, fetchFunc: (params?: any) => Promise<any>,
mapDataFunc: (data: Partial<T>) => T = (data) => data as T, mapDataFunc: (data: Partial<T>) => T = (data) => data as T,
pollingInterval: number = 30000, // 默认30秒轮询一次 pollingInterval: number = 30000, // 默认30秒轮询一次
autoRefresh: boolean = false, // 是否自动开始轮询,默认 false autoRefresh: boolean = false, // 是否自动开始轮询,默认 false
additionalPollingFuncs: (() => Promise<any>)[] = [], // 额外的轮询函数 additionalPollingFuncs: (() => Promise<any>)[] = [], // 额外的轮询函数
pageOffset: number = 1 pageOffset: number = 1
) { ) {
const { message } = App.useApp(); const { message } = App.useApp();
// 轮询相关状态 // 轮询相关状态
const [isPolling, setIsPolling] = useState(false); const [isPolling, setIsPolling] = useState(false);
const pollingTimerRef = useRef<NodeJS.Timeout | null>(null); const pollingTimerRef = useRef<NodeJS.Timeout | null>(null);
// 表格数据 // 表格数据
const [tableData, setTableData] = useState<T[]>([]); const [tableData, setTableData] = useState<T[]>([]);
// 设置加载状态 // 设置加载状态
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
// 搜索参数 // 搜索参数
const [searchParams, setSearchParams] = useState({ const [searchParams, setSearchParams] = useState({
keyword: "", keyword: "",
filter: { filter: {
type: [] as string[], type: [] as string[],
status: [] as string[], status: [] as string[],
tags: [] as string[], tags: [] as string[],
// 通用分类筛选(如算子市场的分类 ID 列表) },
categories: [] as string[], current: 1,
}, pageSize: 12,
current: 1, });
pageSize: 12,
}); // 分页配置
const [pagination, setPagination] = useState({
// 分页配置 total: 0,
const [pagination, setPagination] = useState({ showSizeChanger: true,
total: 0, pageSizeOptions: ["12", "24", "48"],
showSizeChanger: true, showTotal: (total: number) => `${total}`,
pageSizeOptions: ["12", "24", "48"], onChange: (current: number, pageSize?: number) => {
showTotal: (total: number) => `${total}`, setSearchParams((prev) => ({
onChange: (current: number, pageSize?: number) => { ...prev,
setSearchParams((prev) => ({ current,
...prev, pageSize: pageSize || prev.pageSize,
current, }));
pageSize: pageSize || prev.pageSize, },
})); });
},
}); const handleFiltersChange = (searchFilters: { [key: string]: string[] }) => {
setSearchParams({
const handleFiltersChange = (searchFilters: { [key: string]: string[] }) => { ...searchParams,
setSearchParams({ current: 1,
...searchParams, filter: { ...searchParams.filter, ...searchFilters },
current: 1, });
filter: { ...searchParams.filter, ...searchFilters }, };
});
}; const handleKeywordChange = (keyword: string) => {
setSearchParams({
const handleKeywordChange = (keyword: string) => { ...searchParams,
setSearchParams({ current: 1,
...searchParams, keyword: keyword,
current: 1, });
keyword: keyword, };
});
}; function getFirstOfArray(arr: string[]) {
if (!arr || arr.length === 0 || !Array.isArray(arr)) return undefined;
function getFirstOfArray(arr: string[]) { if (arr[0] === "all") return undefined;
if (!arr || arr.length === 0 || !Array.isArray(arr)) return undefined; return arr[0];
if (arr[0] === "all") return undefined; }
return arr[0];
} // 清除轮询定时器
const clearPollingTimer = useCallback(() => {
// 清除轮询定时器 if (pollingTimerRef.current) {
const clearPollingTimer = useCallback(() => { clearTimeout(pollingTimerRef.current);
if (pollingTimerRef.current) { pollingTimerRef.current = null;
clearTimeout(pollingTimerRef.current); }
pollingTimerRef.current = null; }, []);
}
}, []); const fetchData = useCallback(
async (extraParams = {}, skipPollingRestart = false) => {
const fetchData = useCallback( const { keyword, filter, current, pageSize } = searchParams;
async (extraParams = {}, skipPollingRestart = false) => { if (!skipPollingRestart) {
const { keyword, filter, current, pageSize } = searchParams; Loading.show();
if (!skipPollingRestart) { setLoading(true);
Loading.show(); }
setLoading(true);
} // 如果正在轮询且不是轮询触发的调用,先停止当前轮询
const wasPolling = isPolling && !skipPollingRestart;
// 如果正在轮询且不是轮询触发的调用,先停止当前轮询 if (wasPolling) {
const wasPolling = isPolling && !skipPollingRestart; clearPollingTimer();
if (wasPolling) { }
clearPollingTimer();
} try {
// 同时执行主要数据获取和额外的轮询函数
try { const promises = [
// 同时执行主要数据获取和额外的轮询函数 fetchFunc({
const promises = [ ...Object.fromEntries(
fetchFunc({ Object.entries(filter).filter(([_, value]) => value != null && value.length > 0)
...Object.fromEntries( ),
Object.entries(filter).filter(([_, value]) => value != null && value.length > 0) ...extraParams,
), keyword,
...extraParams, type: getFirstOfArray(filter?.type) || undefined,
keyword, status: getFirstOfArray(filter?.status) || undefined,
type: getFirstOfArray(filter?.type) || undefined, tags: filter?.tags?.length ? filter.tags.join(",") : undefined,
status: getFirstOfArray(filter?.status) || undefined, page: current - pageOffset,
tags: filter?.tags?.length ? filter.tags.join(",") : undefined, size: pageSize, // Use camelCase for HTTP query params
page: current - pageOffset, }),
size: pageSize, // Use camelCase for HTTP query params ...additionalPollingFuncs.map((func) => func()),
}), ];
...additionalPollingFuncs.map((func) => func()),
]; const results = await Promise.all(promises);
const { data } = results[0]; // 主要数据结果
const results = await Promise.all(promises);
const { data } = results[0]; // 主要数据结果 setPagination((prev) => ({
...prev,
setPagination((prev) => ({ total: data?.totalElements || 0,
...prev, }));
total: data?.totalElements || 0, let result = [];
})); if (mapDataFunc) {
let result = []; result = data?.content.map(mapDataFunc) ?? [];
if (mapDataFunc) { }
result = data?.content.map(mapDataFunc) ?? []; setTableData(result);
}
setTableData(result); // 如果之前正在轮询且不是轮询触发的调用,重新开始轮询
if (wasPolling) {
// 如果之前正在轮询且不是轮询触发的调用,重新开始轮询 const poll = () => {
if (wasPolling) { pollingTimerRef.current = setTimeout(() => {
const poll = () => { fetchData({}, true).then(() => {
pollingTimerRef.current = setTimeout(() => { if (pollingTimerRef.current) {
fetchData({}, true).then(() => { poll();
if (pollingTimerRef.current) { }
poll(); });
} }, pollingInterval);
}); };
}, pollingInterval); poll();
}; }
poll(); } catch (error) {
} console.error(error);
} catch (error) { message.error("数据获取失败,请稍后重试");
console.error(error); } finally {
message.error("数据获取失败,请稍后重试"); Loading.hide();
} finally { setLoading(false);
Loading.hide(); }
setLoading(false); },
} [
}, searchParams,
[ fetchFunc,
searchParams, mapDataFunc,
fetchFunc, isPolling,
mapDataFunc, clearPollingTimer,
isPolling, pollingInterval,
clearPollingTimer, message,
pollingInterval, additionalPollingFuncs,
message, ]
additionalPollingFuncs, );
]
); // 开始轮询
const startPolling = useCallback(() => {
// 开始轮询 clearPollingTimer();
const startPolling = useCallback(() => { setIsPolling(true);
clearPollingTimer();
setIsPolling(true); const poll = () => {
pollingTimerRef.current = setTimeout(() => {
const poll = () => { fetchData({}, true).then(() => {
pollingTimerRef.current = setTimeout(() => { if (pollingTimerRef.current) {
fetchData({}, true).then(() => { poll();
if (pollingTimerRef.current) { }
poll(); });
} }, pollingInterval);
}); };
}, pollingInterval);
}; poll();
}, [pollingInterval, clearPollingTimer, fetchData]);
poll();
}, [pollingInterval, clearPollingTimer, fetchData]); // 停止轮询
const stopPolling = useCallback(() => {
// 停止轮询 clearPollingTimer();
const stopPolling = useCallback(() => { setIsPolling(false);
clearPollingTimer(); }, [clearPollingTimer]);
setIsPolling(false);
}, [clearPollingTimer]); // 搜索参数变化时,自动刷新数据
// keyword 变化时,防抖500ms后刷新
// 搜索参数变化时,自动刷新数据 useDebouncedEffect(
// keyword 变化时,防抖500ms后刷新 () => {
useDebouncedEffect( fetchData();
() => { },
fetchData(); [searchParams],
}, searchParams?.keyword ? 500 : 0
[searchParams], );
searchParams?.keyword ? 500 : 0
); // 组件卸载时清理轮询
useEffect(() => {
// 组件卸载时清理轮询 if (autoRefresh) {
useEffect(() => { startPolling();
if (autoRefresh) { }
startPolling(); return () => {
} clearPollingTimer();
return () => { };
clearPollingTimer(); }, [clearPollingTimer]);
};
}, [clearPollingTimer]); return {
loading,
return { tableData,
loading, pagination: {
tableData, ...pagination,
pagination: { current: searchParams.current,
...pagination, pageSize: searchParams.pageSize,
current: searchParams.current, },
pageSize: searchParams.pageSize, searchParams,
}, setSearchParams,
searchParams, setPagination,
setSearchParams, handleFiltersChange,
setPagination, handleKeywordChange,
handleFiltersChange, fetchData,
handleKeywordChange, isPolling,
fetchData, startPolling,
isPolling, stopPolling,
startPolling, };
stopPolling, }
};
}

View File

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

View File

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

View File

@@ -1,187 +1,187 @@
import { TaskItem } from "@/pages/DataManagement/dataset.model"; import { TaskItem } from "@/pages/DataManagement/dataset.model";
import { calculateSHA256, checkIsFilesExist } from "@/utils/file.util"; import { calculateSHA256, checkIsFilesExist } from "@/utils/file.util";
import { App } from "antd"; import { App } from "antd";
import { useRef, useState } from "react"; import { useRef, useState } from "react";
export function useFileSliceUpload( export function useFileSliceUpload(
{ {
preUpload, preUpload,
uploadChunk, uploadChunk,
cancelUpload, cancelUpload,
}: { }: {
preUpload: (id: string, params: any) => Promise<{ data: number }>; preUpload: (id: string, params: any) => Promise<{ data: number }>;
uploadChunk: (id: string, formData: FormData, config: any) => Promise<any>; uploadChunk: (id: string, formData: FormData, config: any) => Promise<any>;
cancelUpload: ((reqId: number) => Promise<any>) | null; cancelUpload: ((reqId: number) => Promise<any>) | null;
}, },
showTaskCenter = true // 上传时是否显示任务中心 showTaskCenter = true // 上传时是否显示任务中心
) { ) {
const { message } = App.useApp(); const { message } = App.useApp();
const [taskList, setTaskList] = useState<TaskItem[]>([]); const [taskList, setTaskList] = useState<TaskItem[]>([]);
const taskListRef = useRef<TaskItem[]>([]); // 用于固定任务顺序 const taskListRef = useRef<TaskItem[]>([]); // 用于固定任务顺序
const createTask = (detail: any = {}) => { const createTask = (detail: any = {}) => {
const { dataset } = detail; const { dataset } = detail;
const title = `上传数据集: ${dataset.name} `; const title = `上传数据集: ${dataset.name} `;
const controller = new AbortController(); const controller = new AbortController();
const task: TaskItem = { const task: TaskItem = {
key: dataset.id, key: dataset.id,
title, title,
percent: 0, percent: 0,
reqId: -1, reqId: -1,
controller, controller,
size: 0, size: 0,
updateEvent: detail.updateEvent, updateEvent: detail.updateEvent,
hasArchive: detail.hasArchive, hasArchive: detail.hasArchive,
}; };
taskListRef.current = [task, ...taskListRef.current]; taskListRef.current = [task, ...taskListRef.current];
setTaskList(taskListRef.current); setTaskList(taskListRef.current);
return task; return task;
}; };
const updateTaskList = (task: TaskItem) => { const updateTaskList = (task: TaskItem) => {
taskListRef.current = taskListRef.current.map((item) => taskListRef.current = taskListRef.current.map((item) =>
item.key === task.key ? task : item item.key === task.key ? task : item
); );
setTaskList(taskListRef.current); setTaskList(taskListRef.current);
}; };
const removeTask = (task: TaskItem) => { const removeTask = (task: TaskItem) => {
const { key } = task; const { key } = task;
taskListRef.current = taskListRef.current.filter( taskListRef.current = taskListRef.current.filter(
(item) => item.key !== key (item) => item.key !== key
); );
setTaskList(taskListRef.current); setTaskList(taskListRef.current);
if (task.isCancel && task.cancelFn) { if (task.isCancel && task.cancelFn) {
task.cancelFn(); task.cancelFn();
} }
if (task.updateEvent) window.dispatchEvent(new Event(task.updateEvent)); if (task.updateEvent) window.dispatchEvent(new Event(task.updateEvent));
if (showTaskCenter) { if (showTaskCenter) {
window.dispatchEvent( window.dispatchEvent(
new CustomEvent("show:task-popover", { detail: { show: false } }) new CustomEvent("show:task-popover", { detail: { show: false } })
); );
} }
}; };
async function buildFormData({ file, reqId, i, j }) { async function buildFormData({ file, reqId, i, j }) {
const formData = new FormData(); const formData = new FormData();
const { slices, name, size } = file; const { slices, name, size } = file;
const checkSum = await calculateSHA256(slices[j]); const checkSum = await calculateSHA256(slices[j]);
formData.append("file", slices[j]); formData.append("file", slices[j]);
formData.append("reqId", reqId.toString()); formData.append("reqId", reqId.toString());
formData.append("fileNo", (i + 1).toString()); formData.append("fileNo", (i + 1).toString());
formData.append("chunkNo", (j + 1).toString()); formData.append("chunkNo", (j + 1).toString());
formData.append("fileName", name); formData.append("fileName", name);
formData.append("fileSize", size.toString()); formData.append("fileSize", size.toString());
formData.append("totalChunkNum", slices.length.toString()); formData.append("totalChunkNum", slices.length.toString());
formData.append("checkSumHex", checkSum); formData.append("checkSumHex", checkSum);
return formData; return formData;
} }
async function uploadSlice(task: TaskItem, fileInfo) { async function uploadSlice(task: TaskItem, fileInfo) {
if (!task) { if (!task) {
return; return;
} }
const { reqId, key } = task; const { reqId, key } = task;
const { loaded, i, j, files, totalSize } = fileInfo; const { loaded, i, j, files, totalSize } = fileInfo;
const formData = await buildFormData({ const formData = await buildFormData({
file: files[i], file: files[i],
i, i,
j, j,
reqId, reqId,
}); });
let newTask = { ...task }; let newTask = { ...task };
await uploadChunk(key, formData, { await uploadChunk(key, formData, {
onUploadProgress: (e) => { onUploadProgress: (e) => {
const loadedSize = loaded + e.loaded; const loadedSize = loaded + e.loaded;
const curPercent = Number((loadedSize / totalSize) * 100).toFixed(2); const curPercent = Number((loadedSize / totalSize) * 100).toFixed(2);
newTask = { newTask = {
...newTask, ...newTask,
...taskListRef.current.find((item) => item.key === key), ...taskListRef.current.find((item) => item.key === key),
size: loadedSize, size: loadedSize,
percent: curPercent >= 100 ? 99.99 : curPercent, percent: curPercent >= 100 ? 99.99 : curPercent,
}; };
updateTaskList(newTask); updateTaskList(newTask);
}, },
}); });
} }
async function uploadFile({ task, files, totalSize }) { async function uploadFile({ task, files, totalSize }) {
const { data: reqId } = await preUpload(task.key, { const { data: reqId } = await preUpload(task.key, {
totalFileNum: files.length, totalFileNum: files.length,
totalSize, totalSize,
datasetId: task.key, datasetId: task.key,
hasArchive: task.hasArchive, hasArchive: task.hasArchive,
}); });
const newTask: TaskItem = { const newTask: TaskItem = {
...task, ...task,
reqId, reqId,
isCancel: false, isCancel: false,
cancelFn: () => { cancelFn: () => {
task.controller.abort(); task.controller.abort();
cancelUpload?.(reqId); cancelUpload?.(reqId);
if (task.updateEvent) window.dispatchEvent(new Event(task.updateEvent)); if (task.updateEvent) window.dispatchEvent(new Event(task.updateEvent));
}, },
}; };
updateTaskList(newTask); updateTaskList(newTask);
if (showTaskCenter) { if (showTaskCenter) {
window.dispatchEvent( window.dispatchEvent(
new CustomEvent("show:task-popover", { detail: { show: true } }) new CustomEvent("show:task-popover", { detail: { show: true } })
); );
} }
// // 更新数据状态 // // 更新数据状态
if (task.updateEvent) window.dispatchEvent(new Event(task.updateEvent)); if (task.updateEvent) window.dispatchEvent(new Event(task.updateEvent));
let loaded = 0; let loaded = 0;
for (let i = 0; i < files.length; i++) { for (let i = 0; i < files.length; i++) {
const { slices } = files[i]; const { slices } = files[i];
for (let j = 0; j < slices.length; j++) { for (let j = 0; j < slices.length; j++) {
await uploadSlice(newTask, { await uploadSlice(newTask, {
loaded, loaded,
i, i,
j, j,
files, files,
totalSize, totalSize,
}); });
loaded += slices[j].size; loaded += slices[j].size;
} }
} }
removeTask(newTask); removeTask(newTask);
} }
const handleUpload = async ({ task, files }) => { const handleUpload = async ({ task, files }) => {
const isErrorFile = await checkIsFilesExist(files); const isErrorFile = await checkIsFilesExist(files);
if (isErrorFile) { if (isErrorFile) {
message.error("文件被修改或删除,请重新选择文件上传"); message.error("文件被修改或删除,请重新选择文件上传");
removeTask({ removeTask({
...task, ...task,
isCancel: false, isCancel: false,
...taskListRef.current.find((item) => item.key === task.key), ...taskListRef.current.find((item) => item.key === task.key),
}); });
return; return;
} }
try { try {
const totalSize = files.reduce((acc, file) => acc + file.size, 0); const totalSize = files.reduce((acc, file) => acc + file.size, 0);
await uploadFile({ task, files, totalSize }); await uploadFile({ task, files, totalSize });
} catch (err) { } catch (err) {
console.error(err); console.error(err);
message.error("文件上传失败,请稍后重试"); message.error("文件上传失败,请稍后重试");
removeTask({ removeTask({
...task, ...task,
isCancel: true, isCancel: true,
...taskListRef.current.find((item) => item.key === task.key), ...taskListRef.current.find((item) => item.key === task.key),
}); });
} }
}; };
return { return {
taskList, taskList,
createTask, createTask,
removeTask, removeTask,
handleUpload, handleUpload,
}; };
} }

View File

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

View File

@@ -1,67 +1,67 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { message } from "antd"; import { message } from "antd";
import { getTagConfigUsingGet } from "../pages/DataAnnotation/annotation.api"; import { getTagConfigUsingGet } from "../pages/DataAnnotation/annotation.api";
import type { LabelStudioTagConfig } from "../pages/DataAnnotation/annotation.tagconfig"; import type { LabelStudioTagConfig } from "../pages/DataAnnotation/annotation.tagconfig";
import { parseTagConfig, type TagOption } from "../pages/DataAnnotation/annotation.tagconfig"; import { parseTagConfig, type TagOption } from "../pages/DataAnnotation/annotation.tagconfig";
interface UseTagConfigReturn { interface UseTagConfigReturn {
config: LabelStudioTagConfig | null; config: LabelStudioTagConfig | null;
objectOptions: TagOption[]; objectOptions: TagOption[];
controlOptions: TagOption[]; controlOptions: TagOption[];
loading: boolean; loading: boolean;
error: string | null; error: string | null;
refetch: () => Promise<void>; refetch: () => Promise<void>;
} }
/** /**
* Hook to fetch and manage Label Studio tag configuration * Hook to fetch and manage Label Studio tag configuration
* @param includeLabelingOnly - If true, only include controls with category="labeling" (default: true) * @param includeLabelingOnly - If true, only include controls with category="labeling" (default: true)
*/ */
export function useTagConfig(includeLabelingOnly: boolean = true): UseTagConfigReturn { export function useTagConfig(includeLabelingOnly: boolean = true): UseTagConfigReturn {
const [config, setConfig] = useState<LabelStudioTagConfig | null>(null); const [config, setConfig] = useState<LabelStudioTagConfig | null>(null);
const [objectOptions, setObjectOptions] = useState<TagOption[]>([]); const [objectOptions, setObjectOptions] = useState<TagOption[]>([]);
const [controlOptions, setControlOptions] = useState<TagOption[]>([]); const [controlOptions, setControlOptions] = useState<TagOption[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const fetchConfig = async () => { const fetchConfig = async () => {
setLoading(true); setLoading(true);
setError(null); setError(null);
try { try {
const response = await getTagConfigUsingGet(); const response = await getTagConfigUsingGet();
if (response.code === 200 && response.data) { if (response.code === 200 && response.data) {
const tagConfig: LabelStudioTagConfig = response.data; const tagConfig: LabelStudioTagConfig = response.data;
setConfig(tagConfig); setConfig(tagConfig);
const { objectOptions: objects, controlOptions: controls } = const { objectOptions: objects, controlOptions: controls } =
parseTagConfig(tagConfig, includeLabelingOnly); parseTagConfig(tagConfig, includeLabelingOnly);
setObjectOptions(objects); setObjectOptions(objects);
setControlOptions(controls); setControlOptions(controls);
} else { } else {
const errorMsg = response.message || "获取标签配置失败"; const errorMsg = response.message || "获取标签配置失败";
setError(errorMsg); setError(errorMsg);
message.error(errorMsg); message.error(errorMsg);
} }
} catch (err: any) { } catch (err: any) {
const errorMsg = err.message || "加载标签配置时出错"; const errorMsg = err.message || "加载标签配置时出错";
setError(errorMsg); setError(errorMsg);
console.error("Failed to fetch tag config:", err); console.error("Failed to fetch tag config:", err);
message.error(errorMsg); message.error(errorMsg);
} finally { } finally {
setLoading(false); setLoading(false);
} }
}; };
useEffect(() => { useEffect(() => {
fetchConfig(); fetchConfig();
}, []); }, []);
return { return {
config, config,
objectOptions, objectOptions,
controlOptions, controlOptions,
loading, loading,
error, error,
refetch: fetchConfig, refetch: fetchConfig,
}; };
} }

View File

@@ -1,75 +1,75 @@
@import "tailwindcss"; @import "tailwindcss";
/* components/TopLoadingBar.css */ /* components/TopLoadingBar.css */
.top-loading-bar { .top-loading-bar {
position: fixed; position: fixed;
top: 0; top: 0;
left: 0; left: 0;
width: 100%; width: 100%;
height: 3px; height: 3px;
background-color: transparent; background-color: transparent;
z-index: 9999; z-index: 9999;
overflow: hidden; overflow: hidden;
} }
.loading-bar-progress { .loading-bar-progress {
height: 100%; height: 100%;
background: linear-gradient(90deg, #3498db, #2ecc71, #3498db); background: linear-gradient(90deg, #3498db, #2ecc71, #3498db);
background-size: 200% 100%; background-size: 200% 100%;
animation: gradient-animation 2s linear infinite, width-animation 0.3s ease; animation: gradient-animation 2s linear infinite, width-animation 0.3s ease;
transition: width 0.3s ease; transition: width 0.3s ease;
} }
@keyframes gradient-animation { @keyframes gradient-animation {
0% { 0% {
background-position: 200% 0; background-position: 200% 0;
} }
100% { 100% {
background-position: -200% 0; background-position: -200% 0;
} }
} }
@keyframes width-animation { @keyframes width-animation {
from { from {
transform: translateX(-100%); transform: translateX(-100%);
} }
to { to {
transform: translateX(0); transform: translateX(0);
} }
} }
.show-task-popover { .show-task-popover {
opacity: 100%; opacity: 100%;
visibility: visible; visibility: visible;
transform: translateX(0); transform: translateX(0);
} }
@layer components { @layer components {
.flex-center { .flex-center {
@apply flex items-center justify-center; @apply flex items-center justify-center;
} }
.flex-overflow-auto { .flex-overflow-auto {
@apply flex-1 flex flex-col overflow-auto h-full; @apply flex-1 flex flex-col overflow-auto h-full;
} }
.flex-overflow-hidden { .flex-overflow-hidden {
@apply flex flex-col h-full overflow-hidden; @apply flex flex-col h-full overflow-hidden;
} }
.border-card { .border-card {
@apply border border-[#f0f0f0] rounded-lg bg-white; @apply border border-[#f0f0f0] rounded-lg bg-white;
} }
.border { .border {
@apply border border-[#f0f0f0]; @apply border border-[#f0f0f0];
} }
.border-bottom { .border-bottom {
@apply border-b border-[#f0f0f0]; @apply border-b border-[#f0f0f0];
} }
.border-top { .border-top {
@apply border-t border-[#f0f0f0]; @apply border-t border-[#f0f0f0];
} }
.border-right { .border-right {
@apply border-r border-[#f0f0f0]; @apply border-r border-[#f0f0f0];
} }
.border-left { .border-left {
@apply border-l border-[#f0f0f0]; @apply border-l border-[#f0f0f0];
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,14 +1,14 @@
const setHeader = (req, res, next) => { const setHeader = (req, res, next) => {
res.set({ res.set({
'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET,POST,PUT,DELETE,OPTIONS', 'Access-Control-Allow-Methods': 'GET,POST,PUT,DELETE,OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization', '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';", '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-Content-Type-Options': 'nosniff',
'X-Frame-Options': 'SAMEORIGIN', 'X-Frame-Options': 'SAMEORIGIN',
'X-XSS-Protection': '1; mode=block', 'X-XSS-Protection': '1; mode=block',
}); });
next(); next();
}; };
module.exports = setHeader; module.exports = setHeader;

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,232 +1,232 @@
const Mock = require("mockjs"); const Mock = require("mockjs");
const API = require("../mock-apis.cjs"); const API = require("../mock-apis.cjs");
const { Random } = Mock; const { Random } = Mock;
// 生成模拟数据归集统计信息 // 生成模拟数据归集统计信息
function dataXTemplate() { function dataXTemplate() {
return { return {
id: Mock.Random.guid().replace(/[^a-zA-Z0-9]/g, ""), id: Mock.Random.guid().replace(/[^a-zA-Z0-9]/g, ""),
name: Mock.Random.ctitle(5, 15), name: Mock.Random.ctitle(5, 15),
sourceType: Mock.Random.csentence(3, 10), sourceType: Mock.Random.csentence(3, 10),
targetType: Mock.Random.csentence(3, 10), targetType: Mock.Random.csentence(3, 10),
description: Mock.Random.csentence(5, 20), description: Mock.Random.csentence(5, 20),
version: `v${Mock.Random.integer(1, 5)}.${Mock.Random.integer( version: `v${Mock.Random.integer(1, 5)}.${Mock.Random.integer(
0, 0,
9 9
)}.${Mock.Random.integer(0, 9)}`, )}.${Mock.Random.integer(0, 9)}`,
isSystem: Mock.Random.boolean(), isSystem: Mock.Random.boolean(),
createdAt: 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"), updatedAt: Mock.Random.datetime("yyyy-MM-dd HH:mm:ss"),
}; };
} }
const templateList = new Array(20).fill(null).map(dataXTemplate); const templateList = new Array(20).fill(null).map(dataXTemplate);
// 生成模拟任务数据 // 生成模拟任务数据
function taskItem() { function taskItem() {
return { return {
id: Mock.Random.guid().replace(/[^a-zA-Z0-9]/g, ""), id: Mock.Random.guid().replace(/[^a-zA-Z0-9]/g, ""),
name: Mock.Random.ctitle(5, 20), name: Mock.Random.ctitle(5, 20),
description: Mock.Random.csentence(5, 20), description: Mock.Random.csentence(5, 20),
syncMode: Mock.Random.pick(["ONCE", "SCHEDULED"]), syncMode: Mock.Random.pick(["ONCE", "SCHEDULED"]),
config: { config: {
query: "SELECT * FROM table WHERE condition", query: "SELECT * FROM table WHERE condition",
batchSize: Mock.Random.integer(100, 1000), batchSize: Mock.Random.integer(100, 1000),
frequency: Mock.Random.integer(1, 60), // in minutes frequency: Mock.Random.integer(1, 60), // in minutes
}, },
scheduleExpression: "0 0 * * *", // cron expression scheduleExpression: "0 0 * * *", // cron expression
lastExecutionId: Mock.Random.guid().replace(/[^a-zA-Z0-9]/g, ""), lastExecutionId: Mock.Random.guid().replace(/[^a-zA-Z0-9]/g, ""),
status: Mock.Random.pick([ status: Mock.Random.pick([
"DRAFT", "DRAFT",
"READY", "READY",
"RUNNING", "RUNNING",
"FAILED", "FAILED",
"STOPPED", "STOPPED",
"SUCCESS", "SUCCESS",
]), ]),
createdAt: 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"), updatedAt: Mock.Random.datetime("yyyy-MM-dd HH:mm:ss"),
sourceDataSourceId: Mock.Random.guid().replace(/[^a-zA-Z0-9]/g, ""), sourceDataSourceId: Mock.Random.guid().replace(/[^a-zA-Z0-9]/g, ""),
sourceDataSourceName: Mock.Random.ctitle(5, 20), sourceDataSourceName: Mock.Random.ctitle(5, 20),
targetDataSourceId: Mock.Random.guid().replace(/[^a-zA-Z0-9]/g, ""), targetDataSourceId: Mock.Random.guid().replace(/[^a-zA-Z0-9]/g, ""),
targetDataSourceName: Mock.Random.ctitle(5, 20), targetDataSourceName: Mock.Random.ctitle(5, 20),
}; };
} }
const taskList = new Array(50).fill(null).map(taskItem); const taskList = new Array(50).fill(null).map(taskItem);
// 生成模拟任务执行日志数据 // 生成模拟任务执行日志数据
function executionLogItem() { function executionLogItem() {
return { return {
id: Mock.Random.guid().replace(/[^a-zA-Z0-9]/g, ""), id: Mock.Random.guid().replace(/[^a-zA-Z0-9]/g, ""),
taskName: Mock.Random.ctitle(5, 20), taskName: Mock.Random.ctitle(5, 20),
dataSource: Mock.Random.ctitle(5, 15), dataSource: Mock.Random.ctitle(5, 15),
startTime: Mock.Random.datetime("yyyy-MM-dd HH:mm:ss"), startTime: Mock.Random.datetime("yyyy-MM-dd HH:mm:ss"),
endTime: 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"]), status: Mock.Random.pick(["SUCCESS", "FAILED", "RUNNING"]),
triggerType: Mock.Random.pick(["MANUAL", "SCHEDULED", "API"]), triggerType: Mock.Random.pick(["MANUAL", "SCHEDULED", "API"]),
duration: Mock.Random.integer(1, 120), duration: Mock.Random.integer(1, 120),
retryCount: Mock.Random.integer(0, 5), retryCount: Mock.Random.integer(0, 5),
recordsProcessed: Mock.Random.integer(100, 10000), recordsProcessed: Mock.Random.integer(100, 10000),
processId: Mock.Random.integer(1000, 9999), processId: Mock.Random.integer(1000, 9999),
errorMessage: Mock.Random.boolean() ? "" : Mock.Random.csentence(5, 20), errorMessage: Mock.Random.boolean() ? "" : Mock.Random.csentence(5, 20),
}; };
} }
const executionLogList = new Array(100).fill(null).map(executionLogItem); const executionLogList = new Array(100).fill(null).map(executionLogItem);
module.exports = function (router) { module.exports = function (router) {
// 获取任务列表 // 获取任务列表
router.get(API.queryTasksUsingGet, (req, res) => { router.get(API.queryTasksUsingGet, (req, res) => {
const { keyword, status, page = 0, size = 10 } = req.query; const { keyword, status, page = 0, size = 10 } = req.query;
let filteredTasks = taskList; let filteredTasks = taskList;
if (keyword) { if (keyword) {
filteredTasks = filteredTasks.filter((task) => filteredTasks = filteredTasks.filter((task) =>
task.name.includes(keyword) task.name.includes(keyword)
); );
} }
if (status && status.length > 0) { if (status && status.length > 0) {
filteredTasks = filteredTasks.filter((task) => filteredTasks = filteredTasks.filter((task) =>
status.includes(task.status) status.includes(task.status)
); );
} }
const startIndex = page * size; const startIndex = page * size;
const endIndex = startIndex + size; const endIndex = startIndex + size;
const paginatedTasks = filteredTasks.slice(startIndex, endIndex); const paginatedTasks = filteredTasks.slice(startIndex, endIndex);
res.send({ res.send({
code: "0", code: "0",
msg: "Success", msg: "Success",
data: { data: {
totalElements: filteredTasks.length, totalElements: filteredTasks.length,
page, page,
size, size,
content: paginatedTasks, content: paginatedTasks,
}, },
}); });
}); });
router.get(API.queryDataXTemplatesUsingGet, (req, res) => { router.get(API.queryDataXTemplatesUsingGet, (req, res) => {
const { keyword, page = 0, size = 10 } = req.query; const { keyword, page = 0, size = 10 } = req.query;
let filteredTemplates = templateList; let filteredTemplates = templateList;
if (keyword) { if (keyword) {
filteredTemplates = filteredTemplates.filter((template) => filteredTemplates = filteredTemplates.filter((template) =>
template.name.includes(keyword) template.name.includes(keyword)
); );
} }
const startIndex = page * size; const startIndex = page * size;
const endIndex = startIndex + size; const endIndex = startIndex + size;
const paginatedTemplates = filteredTemplates.slice(startIndex, endIndex); const paginatedTemplates = filteredTemplates.slice(startIndex, endIndex);
res.send({ res.send({
code: "0", code: "0",
msg: "Success", msg: "Success",
data: { data: {
content: paginatedTemplates, content: paginatedTemplates,
totalElements: filteredTemplates.length, totalElements: filteredTemplates.length,
page, page,
size, size,
}, },
}); });
}); });
// 创建任务 // 创建任务
router.post(API.createTaskUsingPost, (req, res) => { router.post(API.createTaskUsingPost, (req, res) => {
taskList.unshift(taskItem()); // 添加一个新的任务到列表开头 taskList.unshift(taskItem()); // 添加一个新的任务到列表开头
res.send({ res.send({
code: "0", code: "0",
msg: "任务创建成功", msg: "任务创建成功",
data: { data: {
id: Mock.Random.guid().replace(/[^a-zA-Z0-9]/g, ""), id: Mock.Random.guid().replace(/[^a-zA-Z0-9]/g, ""),
}, },
}); });
}); });
// 更新任务 // 更新任务
router.post(API.updateTaskByIdUsingPut, (req, res) => { router.post(API.updateTaskByIdUsingPut, (req, res) => {
const { id } = req.body; const { id } = req.body;
res.send({ res.send({
code: "0", code: "0",
msg: "Data source task updated successfully", msg: "Data source task updated successfully",
data: taskList.find((task) => task.id === id), data: taskList.find((task) => task.id === id),
}); });
}); });
// 删除任务 // 删除任务
router.post(API.deleteTaskByIdUsingDelete, (req, res) => { router.post(API.deleteTaskByIdUsingDelete, (req, res) => {
const { id } = req.body; const { id } = req.body;
const index = taskList.findIndex((task) => task.id === id); const index = taskList.findIndex((task) => task.id === id);
if (index !== -1) { if (index !== -1) {
taskList.splice(index, 1); taskList.splice(index, 1);
} }
res.send({ res.send({
code: "0", code: "0",
msg: "Data source task deleted successfully", msg: "Data source task deleted successfully",
data: null, data: null,
}); });
}); });
// 执行任务 // 执行任务
router.post(API.executeTaskByIdUsingPost, (req, res) => { router.post(API.executeTaskByIdUsingPost, (req, res) => {
console.log("Received request to execute task", req.body); console.log("Received request to execute task", req.body);
const { id } = req.body; const { id } = req.body;
console.log("Executing task with ID:", id); console.log("Executing task with ID:", id);
taskList.find((task) => task.id === id).status = "RUNNING"; taskList.find((task) => task.id === id).status = "RUNNING";
res.send({ res.send({
code: "0", code: "0",
msg: "Data source task execution started", msg: "Data source task execution started",
data: null, data: null,
}); });
}); });
// 停止任务 // 停止任务
router.post(API.stopTaskByIdUsingPost, (req, res) => { router.post(API.stopTaskByIdUsingPost, (req, res) => {
const { id } = req.body; const { id } = req.body;
const task = taskList.find((task) => task.id === id); const task = taskList.find((task) => task.id === id);
if (task) { if (task) {
task.status = "STOPPED"; task.status = "STOPPED";
} }
res.send({ res.send({
code: "0", code: "0",
msg: "Data source task stopped successfully", msg: "Data source task stopped successfully",
data: null, data: null,
}); });
}); });
// 获取任务执行日志 // 获取任务执行日志
router.post(API.queryExecutionLogUsingPost, (req, res) => { router.post(API.queryExecutionLogUsingPost, (req, res) => {
const { keyword, page = 1, size = 10, status } = req.body; const { keyword, page = 1, size = 10, status } = req.body;
let filteredLogs = executionLogList; let filteredLogs = executionLogList;
if (keyword) { if (keyword) {
filteredLogs = filteredLogs.filter((log) => filteredLogs = filteredLogs.filter((log) =>
log.taskName.includes(keyword) log.taskName.includes(keyword)
); );
} }
if (status && status.length > 0) { if (status && status.length > 0) {
filteredLogs = filteredLogs.filter((log) => status.includes(log.status)); filteredLogs = filteredLogs.filter((log) => status.includes(log.status));
} }
const startIndex = (page - 1) * size; const startIndex = (page - 1) * size;
const endIndex = startIndex + size; const endIndex = startIndex + size;
const paginatedLogs = filteredLogs.slice(startIndex, endIndex); const paginatedLogs = filteredLogs.slice(startIndex, endIndex);
res.send({ res.send({
code: "0", code: "0",
msg: "Success", msg: "Success",
data: { data: {
totalElements: filteredLogs.length, totalElements: filteredLogs.length,
page, page,
size, size,
results: paginatedLogs, results: paginatedLogs,
}, },
}); });
}); });
// 获取任务执行日志详情 // 获取任务执行日志详情
router.post(API.queryExecutionLogByIdUsingGet, (req, res) => { router.post(API.queryExecutionLogByIdUsingGet, (req, res) => {
const { id } = req.body; const { id } = req.body;
const log = executionLogList.find((log) => log.id === id); const log = executionLogList.find((log) => log.id === id);
res.send({ res.send({
code: "0", code: "0",
msg: "Success", msg: "Success",
data: log, data: log,
}); });
}); });
}; };

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -1,220 +1,220 @@
const Mock = require("mockjs"); const Mock = require("mockjs");
const API = require("../mock-apis.cjs"); const API = require("../mock-apis.cjs");
function ratioJobItem() { function ratioJobItem() {
return { return {
id: Mock.Random.guid().replace(/[^a-zA-Z0-9]/g, ""), id: Mock.Random.guid().replace(/[^a-zA-Z0-9]/g, ""),
name: Mock.Random.ctitle(5, 15), name: Mock.Random.ctitle(5, 15),
description: Mock.Random.csentence(10, 30), description: Mock.Random.csentence(10, 30),
status: Mock.Random.pick(["PENDING", "RUNNING", "COMPLETED", "FAILED", "PAUSED"]), status: Mock.Random.pick(["PENDING", "RUNNING", "COMPLETED", "FAILED", "PAUSED"]),
totals: Mock.Random.integer(1000, 10000), totals: Mock.Random.integer(1000, 10000),
ratio_method: Mock.Random.pick(["DATASET", "TAG"]), ratio_method: Mock.Random.pick(["DATASET", "TAG"]),
target_dataset_id: Mock.Random.guid().replace(/[^a-zA-Z0-9]/g, ""), target_dataset_id: Mock.Random.guid().replace(/[^a-zA-Z0-9]/g, ""),
target_dataset_name: Mock.Random.ctitle(3, 8), target_dataset_name: Mock.Random.ctitle(3, 8),
config: [ config: [
{ {
datasetId: Mock.Random.guid().replace(/[^a-zA-Z0-9]/g, ""), datasetId: Mock.Random.guid().replace(/[^a-zA-Z0-9]/g, ""),
counts: Mock.Random.integer(100, 1000).toString(), counts: Mock.Random.integer(100, 1000).toString(),
filter_conditions: "", filter_conditions: "",
}, },
{ {
datasetId: Mock.Random.guid().replace(/[^a-zA-Z0-9]/g, ""), datasetId: Mock.Random.guid().replace(/[^a-zA-Z0-9]/g, ""),
counts: Mock.Random.integer(100, 1000).toString(), counts: Mock.Random.integer(100, 1000).toString(),
filter_conditions: "", filter_conditions: "",
}, },
], ],
created_at: Mock.Random.datetime("yyyy-MM-dd HH:mm:ss"), created_at: Mock.Random.datetime("yyyy-MM-dd HH:mm:ss"),
updated_at: Mock.Random.datetime("yyyy-MM-dd HH:mm:ss"), updated_at: Mock.Random.datetime("yyyy-MM-dd HH:mm:ss"),
}; };
} }
const ratioJobList = new Array(20).fill(null).map(ratioJobItem); const ratioJobList = new Array(20).fill(null).map(ratioJobItem);
module.exports = function (router) { module.exports = function (router) {
// 获取配比任务列表 // 获取配比任务列表
router.get(API.queryRatioTasksUsingGet, (req, res) => { router.get(API.queryRatioTasksUsingGet, (req, res) => {
const { page = 0, size = 10, status } = req.query; const { page = 0, size = 10, status } = req.query;
let filteredJobs = ratioJobList; let filteredJobs = ratioJobList;
if (status) { if (status) {
filteredJobs = ratioJobList.filter((job) => job.status === status); filteredJobs = ratioJobList.filter((job) => job.status === status);
} }
const startIndex = page * size; const startIndex = page * size;
const endIndex = startIndex + parseInt(size); const endIndex = startIndex + parseInt(size);
const pageData = filteredJobs.slice(startIndex, endIndex); const pageData = filteredJobs.slice(startIndex, endIndex);
res.send({ res.send({
code: "0", code: "0",
msg: "Success", msg: "Success",
data: { data: {
content: pageData, content: pageData,
totalElements: filteredJobs.length, totalElements: filteredJobs.length,
totalPages: Math.ceil(filteredJobs.length / size), totalPages: Math.ceil(filteredJobs.length / size),
size: parseInt(size), size: parseInt(size),
number: parseInt(page), number: parseInt(page),
}, },
}); });
}); });
// 创建配比任务 // 创建配比任务
router.post(API.createRatioTaskUsingPost, (req, res) => { router.post(API.createRatioTaskUsingPost, (req, res) => {
const newJob = { const newJob = {
...ratioJobItem(), ...ratioJobItem(),
...req.body, ...req.body,
id: Mock.Random.guid().replace(/[^a-zA-Z0-9]/g, ""), id: Mock.Random.guid().replace(/[^a-zA-Z0-9]/g, ""),
status: "PENDING", status: "PENDING",
createdAt: new Date().toISOString(), createdAt: new Date().toISOString(),
}; };
ratioJobList.push(newJob); ratioJobList.push(newJob);
res.status(201).send({ res.status(201).send({
code: "0", code: "0",
msg: "Ratio job created successfully", msg: "Ratio job created successfully",
data: newJob, data: newJob,
}); });
}); });
// 获取配比任务详情 // 获取配比任务详情
router.get(API.queryRatioTaskByIdUsingGet, (req, res) => { router.get(API.queryRatioTaskByIdUsingGet, (req, res) => {
const { taskId } = req.params; const { taskId } = req.params;
const job = ratioJobList.find((j) => j.id === taskId); const job = ratioJobList.find((j) => j.id === taskId);
if (job) { if (job) {
res.send({ res.send({
code: "0", code: "0",
msg: "Success", msg: "Success",
data: job, data: job,
}); });
} else { } else {
res.status(404).send({ res.status(404).send({
code: "1", code: "1",
msg: "Ratio job not found", msg: "Ratio job not found",
data: null, data: null,
}); });
} }
}); });
// 删除配比任务 // 删除配比任务
router.delete(API.deleteRatioTaskByIdUsingDelete, (req, res) => { router.delete(API.deleteRatioTaskByIdUsingDelete, (req, res) => {
const { taskId } = req.params; const { taskId } = req.params;
const index = ratioJobList.findIndex((j) => j.id === taskId); const index = ratioJobList.findIndex((j) => j.id === taskId);
if (index !== -1) { if (index !== -1) {
ratioJobList.splice(index, 1); ratioJobList.splice(index, 1);
res.send({ res.send({
code: "0", code: "0",
msg: "Ratio job deleted successfully", msg: "Ratio job deleted successfully",
data: null, data: null,
}); });
} else { } else {
res.status(404).send({ res.status(404).send({
code: "1", code: "1",
msg: "Ratio job not found", msg: "Ratio job not found",
data: null, data: null,
}); });
} }
}); });
// 更新配比任务 // 更新配比任务
router.put(API.updateRatioTaskByIdUsingPut, (req, res) => { router.put(API.updateRatioTaskByIdUsingPut, (req, res) => {
const { taskId } = req.params; const { taskId } = req.params;
const index = ratioJobList.findIndex((j) => j.id === taskId); const index = ratioJobList.findIndex((j) => j.id === taskId);
if (index !== -1) { if (index !== -1) {
ratioJobList[index] = { ratioJobList[index] = {
...ratioJobList[index], ...ratioJobList[index],
...req.body, ...req.body,
updatedAt: new Date().toISOString(), updatedAt: new Date().toISOString(),
}; };
res.send({ res.send({
code: "0", code: "0",
msg: "Ratio job updated successfully", msg: "Ratio job updated successfully",
data: ratioJobList[index], data: ratioJobList[index],
}); });
} else { } else {
res.status(404).send({ res.status(404).send({
code: "1", code: "1",
msg: "Ratio job not found", msg: "Ratio job not found",
data: null, data: null,
}); });
} }
}); });
// 执行配比任务 // 执行配比任务
router.post(API.executeRatioTaskByIdUsingPost, (req, res) => { router.post(API.executeRatioTaskByIdUsingPost, (req, res) => {
const { taskId } = req.params; const { taskId } = req.params;
const job = ratioJobList.find((j) => j.id === taskId); const job = ratioJobList.find((j) => j.id === taskId);
if (job) { if (job) {
job.status = "RUNNING"; job.status = "RUNNING";
job.startedAt = new Date().toISOString(); job.startedAt = new Date().toISOString();
res.send({ res.send({
code: "0", code: "0",
msg: "Ratio job execution started", msg: "Ratio job execution started",
data: { data: {
executionId: Mock.Random.guid().replace(/[^a-zA-Z0-9]/g, ""), executionId: Mock.Random.guid().replace(/[^a-zA-Z0-9]/g, ""),
status: "RUNNING", status: "RUNNING",
message: "Job execution started successfully", message: "Job execution started successfully",
}, },
}); });
} else { } else {
res.status(404).send({ res.status(404).send({
code: "1", code: "1",
msg: "Ratio job not found", msg: "Ratio job not found",
data: null, data: null,
}); });
} }
}); });
// 停止配比任务 // 停止配比任务
router.post(API.stopRatioTaskByIdUsingPost, (req, res) => { router.post(API.stopRatioTaskByIdUsingPost, (req, res) => {
const { taskId } = req.params; const { taskId } = req.params;
const job = ratioJobList.find((j) => j.id === taskId); const job = ratioJobList.find((j) => j.id === taskId);
if (job) { if (job) {
job.status = "STOPPED"; job.status = "STOPPED";
job.finishedAt = new Date().toISOString(); job.finishedAt = new Date().toISOString();
res.send({ res.send({
code: "0", code: "0",
msg: "Ratio job stopped successfully", msg: "Ratio job stopped successfully",
data: null, data: null,
}); });
} else { } else {
res.status(404).send({ res.status(404).send({
code: "1", code: "1",
msg: "Ratio job not found", msg: "Ratio job not found",
data: null, data: null,
}); });
} }
}); });
// 获取配比任务状态 // 获取配比任务状态
router.get(API.queryRatioJobStatusUsingGet, (req, res) => { router.get(API.queryRatioJobStatusUsingGet, (req, res) => {
const { taskId } = req.params; const { taskId } = req.params;
const job = ratioJobList.find((j) => j.id === taskId); const job = ratioJobList.find((j) => j.id === taskId);
if (job) { if (job) {
res.send({ res.send({
code: "0", code: "0",
msg: "Success", msg: "Success",
data: { data: {
status: job.status, status: job.status,
progress: job.progress, progress: job.progress,
}, },
}); });
} else { } else {
res.status(404).send({ res.status(404).send({
code: "1", code: "1",
msg: "Ratio job not found", msg: "Ratio job not found",
data: null, data: null,
}); });
} }
}); });
// 获取配比模型列表 // 获取配比模型列表
router.get(API.queryRatioModelsUsingGet, (req, res) => { router.get(API.queryRatioModelsUsingGet, (req, res) => {
const models = [ const models = [
{ id: "model1", name: "均匀分配模型", description: "将目标数量均匀分配到各数据集。" }, { id: "model1", name: "均匀分配模型", description: "将目标数量均匀分配到各数据集。" },
{ id: "model2", name: "标签优先模型", description: "优先满足标签配比需求。" }, { id: "model2", name: "标签优先模型", description: "优先满足标签配比需求。" },
{ id: "model3", name: "自定义模型", description: "支持自定义分配逻辑。" }, { id: "model3", name: "自定义模型", description: "支持自定义分配逻辑。" },
]; ];
res.send({ res.send({
code: "0", code: "0",
msg: "Success", msg: "Success",
data: models, data: models,
}); });
}); });
}; };

File diff suppressed because it is too large Load Diff

View File

@@ -1,176 +1,176 @@
const Mock = require("mockjs"); const Mock = require("mockjs");
const API = require("../mock-apis.cjs"); const API = require("../mock-apis.cjs");
// 知识库数据 // 知识库数据
function KnowledgeBaseItem() { function KnowledgeBaseItem() {
return { return {
id: Mock.Random.guid().replace(/[^a-zA-Z0-9]/g, ""), id: Mock.Random.guid().replace(/[^a-zA-Z0-9]/g, ""),
name: Mock.Random.ctitle(5, 15), name: Mock.Random.ctitle(5, 15),
description: Mock.Random.csentence(10, 30), description: Mock.Random.csentence(10, 30),
createdBy: Mock.Random.cname(), createdBy: Mock.Random.cname(),
updatedBy: Mock.Random.cname(), updatedBy: Mock.Random.cname(),
embeddingModel: Mock.Random.pick([ embeddingModel: Mock.Random.pick([
"text-embedding-ada-002", "text-embedding-ada-002",
"text-embedding-3-small", "text-embedding-3-small",
"text-embedding-3-large", "text-embedding-3-large",
]), ]),
chatModel: Mock.Random.pick(["gpt-3.5-turbo", "gpt-4", "gpt-4-32k"]), chatModel: Mock.Random.pick(["gpt-3.5-turbo", "gpt-4", "gpt-4-32k"]),
createdAt: 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"), updatedAt: Mock.Random.datetime("yyyy-MM-dd HH:mm:ss"),
}; };
} }
const knowledgeBaseList = new Array(50).fill(null).map(KnowledgeBaseItem); const knowledgeBaseList = new Array(50).fill(null).map(KnowledgeBaseItem);
function fileItem() { function fileItem() {
return { return {
id: Mock.Random.guid().replace(/[^a-zA-Z0-9]/g, ""), id: Mock.Random.guid().replace(/[^a-zA-Z0-9]/g, ""),
createdAt: 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"), updatedAt: Mock.Random.datetime("yyyy-MM-dd HH:mm:ss"),
createdBy: Mock.Random.cname(), createdBy: Mock.Random.cname(),
updatedBy: Mock.Random.cname(), updatedBy: Mock.Random.cname(),
knowledgeBaseId: Mock.Random.pick(knowledgeBaseList).id, knowledgeBaseId: Mock.Random.pick(knowledgeBaseList).id,
fileId: Mock.Random.guid().replace(/[^a-zA-Z0-9]/g, ""), fileId: Mock.Random.guid().replace(/[^a-zA-Z0-9]/g, ""),
fileName: Mock.Random.ctitle(5, 15), fileName: Mock.Random.ctitle(5, 15),
chunkCount: Mock.Random.integer(1, 100), chunkCount: Mock.Random.integer(1, 100),
metadata: {}, metadata: {},
status: Mock.Random.pick([ status: Mock.Random.pick([
"UNPROCESSED", "UNPROCESSED",
"PROCESSING", "PROCESSING",
"PROCESSED", "PROCESSED",
"PROCESS_FAILED", "PROCESS_FAILED",
]), ]),
}; };
} }
const fileList = new Array(20).fill(null).map(fileItem); const fileList = new Array(20).fill(null).map(fileItem);
module.exports = function (router) { module.exports = function (router) {
// 获取知识库列表 // 获取知识库列表
router.post(API.queryKnowledgeBasesUsingPost, (req, res) => { router.post(API.queryKnowledgeBasesUsingPost, (req, res) => {
const { page = 0, size, keyword } = req.body; const { page = 0, size, keyword } = req.body;
let filteredList = knowledgeBaseList; let filteredList = knowledgeBaseList;
if (keyword) { if (keyword) {
filteredList = knowledgeBaseList.filter( filteredList = knowledgeBaseList.filter(
(kb) => kb.name.includes(keyword) || kb.description.includes(keyword) (kb) => kb.name.includes(keyword) || kb.description.includes(keyword)
); );
} }
const start = page * size; const start = page * size;
const end = start + size; const end = start + size;
const totalElements = filteredList.length; const totalElements = filteredList.length;
const paginatedList = filteredList.slice(start, end); const paginatedList = filteredList.slice(start, end);
res.send({ res.send({
code: "0", code: "0",
msg: "Success", msg: "Success",
data: { data: {
totalElements, totalElements,
page, page,
size, size,
content: paginatedList, content: paginatedList,
}, },
}); });
}); });
// 创建知识库 // 创建知识库
router.post(API.createKnowledgeBaseUsingPost, (req, res) => { router.post(API.createKnowledgeBaseUsingPost, (req, res) => {
const item = KnowledgeBaseItem(); const item = KnowledgeBaseItem();
knowledgeBaseList.unshift(item); knowledgeBaseList.unshift(item);
res.status(201).send(item); res.status(201).send(item);
}); });
// 获取知识库详情 // 获取知识库详情
router.get(API.queryKnowledgeBaseByIdUsingGet, (req, res) => { router.get(API.queryKnowledgeBaseByIdUsingGet, (req, res) => {
const id = req.params.baseId; const id = req.params.baseId;
const item = const item =
knowledgeBaseList.find((kb) => kb.id === id) || KnowledgeBaseItem(); knowledgeBaseList.find((kb) => kb.id === id) || KnowledgeBaseItem();
res.send({ res.send({
code: "0", code: "0",
msg: "Success", msg: "Success",
data: item, data: item,
}); });
}); });
// 更新知识库 // 更新知识库
router.put(API.updateKnowledgeBaseByIdUsingPut, (req, res) => { router.put(API.updateKnowledgeBaseByIdUsingPut, (req, res) => {
const id = req.params.baseId; const id = req.params.baseId;
const idx = knowledgeBaseList.findIndex((kb) => kb.id === id); const idx = knowledgeBaseList.findIndex((kb) => kb.id === id);
if (idx >= 0) { if (idx >= 0) {
knowledgeBaseList[idx] = { ...knowledgeBaseList[idx], ...req.body }; knowledgeBaseList[idx] = { ...knowledgeBaseList[idx], ...req.body };
res.status(201).send(knowledgeBaseList[idx]); res.status(201).send(knowledgeBaseList[idx]);
} else { } else {
res.status(404).send({ message: "Not found" }); res.status(404).send({ message: "Not found" });
} }
}); });
// 删除知识库 // 删除知识库
router.delete(API.deleteKnowledgeBaseByIdUsingDelete, (req, res) => { router.delete(API.deleteKnowledgeBaseByIdUsingDelete, (req, res) => {
const id = req.params.baseId; const id = req.params.baseId;
const idx = knowledgeBaseList.findIndex((kb) => kb.id === id); const idx = knowledgeBaseList.findIndex((kb) => kb.id === id);
if (idx >= 0) { if (idx >= 0) {
knowledgeBaseList.splice(idx, 1); knowledgeBaseList.splice(idx, 1);
res.status(201).send({ success: true }); res.status(201).send({ success: true });
} else { } else {
res.status(404).send({ message: "Not found" }); res.status(404).send({ message: "Not found" });
} }
}); });
// 添加文件到知识库 // 添加文件到知识库
router.post(API.addKnowledgeBaseFilesUsingPost, (req, res) => { router.post(API.addKnowledgeBaseFilesUsingPost, (req, res) => {
const file = Mock.mock({ const file = Mock.mock({
id: "@guid", id: "@guid",
name: "@ctitle(5,15)", name: "@ctitle(5,15)",
size: "@integer(1000,1000000)", size: "@integer(1000,1000000)",
status: "uploaded", status: "uploaded",
createdAt: "@datetime", createdAt: "@datetime",
}); });
res.status(201).send(file); res.status(201).send(file);
}); });
// 获取知识生成文件详情 // 获取知识生成文件详情
router.get(API.queryKnowledgeBaseFilesGet, (req, res) => { router.get(API.queryKnowledgeBaseFilesGet, (req, res) => {
const { keyword, page, size } = req.query; const { keyword, page, size } = req.query;
let filteredList = fileList; let filteredList = fileList;
if (keyword) { if (keyword) {
filteredList = fileList.filter((file) => file.fileName.includes(keyword)); filteredList = fileList.filter((file) => file.fileName.includes(keyword));
} }
const start = page * size; const start = page * size;
const end = start + size; const end = start + size;
const totalElements = filteredList.length; const totalElements = filteredList.length;
const paginatedList = filteredList.slice(start, end); const paginatedList = filteredList.slice(start, end);
res.send({ res.send({
code: "0", code: "0",
msg: "Success", msg: "Success",
data: { data: {
totalElements, totalElements,
page, page,
size, size,
content: paginatedList, content: paginatedList,
}, },
}); });
}); });
router.get(API.queryKnowledgeBaseFilesByIdUsingGet, (req, res) => { router.get(API.queryKnowledgeBaseFilesByIdUsingGet, (req, res) => {
const { baseId, fileId } = req.params; const { baseId, fileId } = req.params;
const item = const item =
fileList.find( fileList.find(
(file) => file.knowledgeBaseId === baseId && file.id === fileId (file) => file.knowledgeBaseId === baseId && file.id === fileId
) || fileItem(); ) || fileItem();
res.send({ res.send({
code: "0", code: "0",
msg: "Success", msg: "Success",
data: item, data: item,
}); });
}); });
// 删除知识生成文件 // 删除知识生成文件
router.delete(API.deleteKnowledgeBaseTaskByIdUsingDelete, (req, res) => { router.delete(API.deleteKnowledgeBaseTaskByIdUsingDelete, (req, res) => {
const { id } = req.params; const { id } = req.params;
const idx = fileList.findIndex((file) => file.id === id); const idx = fileList.findIndex((file) => file.id === id);
if (idx >= 0) { if (idx >= 0) {
fileList.splice(idx, 1); fileList.splice(idx, 1);
res.status(200).send({ success: true }); res.status(200).send({ success: true });
return; return;
} }
res.status(404).send({ message: "Not found" }); res.status(404).send({ message: "Not found" });
}); });
}; };

View File

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

View File

@@ -1,177 +1,177 @@
const Mock = require("mockjs"); const Mock = require("mockjs");
const API = require("../mock-apis.cjs"); const API = require("../mock-apis.cjs");
// 算子标签数据 // 算子标签数据
function ModelItem() { function ModelItem() {
return { return {
id: Mock.Random.guid().replace(/[^a-zA-Z0-9]/g, ""), id: Mock.Random.guid().replace(/[^a-zA-Z0-9]/g, ""),
modelName: Mock.Random.pick([ modelName: Mock.Random.pick([
"数据清洗", "数据清洗",
"特征选择", "特征选择",
"分类算法", "分类算法",
"聚类算法", "聚类算法",
"回归分析", "回归分析",
"深度神经网络", "深度神经网络",
"卷积神经网络", "卷积神经网络",
"循环神经网络", "循环神经网络",
"注意力机制", "注意力机制",
"文本分析", "文本分析",
"图像处理", "图像处理",
"语音识别", "语音识别",
"推荐算法", "推荐算法",
"异常检测", "异常检测",
"优化算法", "优化算法",
"集成学习", "集成学习",
"迁移学习", "迁移学习",
"强化学习", "强化学习",
"联邦学习", "联邦学习",
]), ]),
provider: Mock.Random.pick([ provider: Mock.Random.pick([
"OpenAI", "OpenAI",
"Anthropic", "Anthropic",
"Cohere", "Cohere",
"AI21 Labs", "AI21 Labs",
"Hugging Face", "Hugging Face",
"Google Cloud AI", "Google Cloud AI",
"Microsoft Azure AI", "Microsoft Azure AI",
"Amazon Web Services AI", "Amazon Web Services AI",
"IBM Watson", "IBM Watson",
"Alibaba Cloud AI", "Alibaba Cloud AI",
]), ]),
type: Mock.Random.pick(["CHAT", "EMBEDDING"]), type: Mock.Random.pick(["CHAT", "EMBEDDING"]),
usageCount: Mock.Random.integer(1, 500), usageCount: Mock.Random.integer(1, 500),
createdAt: 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"), updatedAt: Mock.Random.datetime("yyyy-MM-dd HH:mm:ss"),
}; };
} }
const modelList = new Array(50).fill(null).map(ModelItem); const modelList = new Array(50).fill(null).map(ModelItem);
function ProviderItem(provider) { function ProviderItem(provider) {
return { return {
id: Mock.Random.guid().replace(/[^a-zA-Z0-9]/g, ""), id: Mock.Random.guid().replace(/[^a-zA-Z0-9]/g, ""),
provider, provider,
baseUrl: Mock.Random.url("https") + "/v1/models", baseUrl: Mock.Random.url("https") + "/v1/models",
createdAt: Mock.Random.datetime("yyyy-MM-dd HH:mm:ss"), createdAt: Mock.Random.datetime("yyyy-MM-dd HH:mm:ss"),
}; };
} }
const ProviderList = [ const ProviderList = [
"OpenAI", "OpenAI",
"Anthropic", "Anthropic",
"Cohere", "Cohere",
"AI21 Labs", "AI21 Labs",
"Hugging Face", "Hugging Face",
"Google Cloud AI", "Google Cloud AI",
"Microsoft Azure AI", "Microsoft Azure AI",
"Amazon Web Services AI", "Amazon Web Services AI",
"IBM Watson", "IBM Watson",
"Alibaba Cloud AI", "Alibaba Cloud AI",
].map(ProviderItem); ].map(ProviderItem);
module.exports = function (router) { module.exports = function (router) {
// 获取模型列表 // 获取模型列表
router.get(API.queryModelsUsingGet, (req, res) => { router.get(API.queryModelsUsingGet, (req, res) => {
const { const {
page = 0, page = 0,
size = 20, size = 20,
keyword = "", keyword = "",
provider = "", provider = "",
type = "", type = "",
} = req.query; } = req.query;
let filteredModels = modelList; let filteredModels = modelList;
if (keyword) { if (keyword) {
filteredModels = modelList.filter((model) => filteredModels = modelList.filter((model) =>
model.modelName.toLowerCase().includes(keyword.toLowerCase()) model.modelName.toLowerCase().includes(keyword.toLowerCase())
); );
} }
if (provider && provider !== "all") { if (provider && provider !== "all") {
filteredModels = filteredModels.filter( filteredModels = filteredModels.filter(
(model) => model.provider === provider (model) => model.provider === provider
); );
} }
if (type && type !== "all") { if (type && type !== "all") {
filteredModels = filteredModels.filter((model) => model.type === type); filteredModels = filteredModels.filter((model) => model.type === type);
} }
const startIndex = page * size; const startIndex = page * size;
const endIndex = startIndex + parseInt(size); const endIndex = startIndex + parseInt(size);
const pageData = filteredModels.slice(startIndex, endIndex); const pageData = filteredModels.slice(startIndex, endIndex);
res.status(201).send({ res.status(201).send({
code: "0", code: "0",
msg: "Success", msg: "Success",
data: { data: {
content: pageData, content: pageData,
totalElements: filteredModels.length, totalElements: filteredModels.length,
totalPages: Math.ceil(filteredModels.length / size), totalPages: Math.ceil(filteredModels.length / size),
size: parseInt(size), size: parseInt(size),
number: parseInt(page), number: parseInt(page),
}, },
}); });
}); });
// 获取模型提供商列表 // 获取模型提供商列表
router.get(API.queryProvidersUsingGet, (req, res) => { router.get(API.queryProvidersUsingGet, (req, res) => {
res.status(201).send({ res.status(201).send({
code: "0", code: "0",
msg: "success", msg: "success",
data: ProviderList, data: ProviderList,
}); });
}); });
// 创建模型 // 创建模型
router.post(API.createModelUsingPost, (req, res) => { router.post(API.createModelUsingPost, (req, res) => {
const { ...modelData } = req.body; const { ...modelData } = req.body;
const newModel = { const newModel = {
id: Mock.Random.guid().replace(/[^a-zA-Z0-9]/g, ""), id: Mock.Random.guid().replace(/[^a-zA-Z0-9]/g, ""),
...modelData, ...modelData,
createdAt: new Date().toISOString(), createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(), updatedAt: new Date().toISOString(),
}; };
modelList.unshift(newModel); modelList.unshift(newModel);
res.status(201).send({ res.status(201).send({
code: "0", code: "0",
msg: "success", msg: "success",
data: newModel, data: newModel,
}); });
}); });
// 删除模型 // 删除模型
router.delete(API.deleteModelUsingDelete, (req, res) => { router.delete(API.deleteModelUsingDelete, (req, res) => {
const { id } = req.params; const { id } = req.params;
const index = modelList.findIndex((model) => model.id === id); const index = modelList.findIndex((model) => model.id === id);
if (index !== -1) { if (index !== -1) {
modelList.splice(index, 1); modelList.splice(index, 1);
} }
res.status(204).send({ res.status(204).send({
code: "0", code: "0",
msg: "success", msg: "success",
data: null, data: null,
}); });
}); });
// 更新模型 // 更新模型
router.put(API.updateModelUsingPut, (req, res) => { router.put(API.updateModelUsingPut, (req, res) => {
const { id, ...update } = req.params; const { id, ...update } = req.params;
const index = modelList.findIndex((model) => model.id === id); const index = modelList.findIndex((model) => model.id === id);
if (index !== -1) { if (index !== -1) {
modelList[index] = { modelList[index] = {
...modelList[index], ...modelList[index],
...update, ...update,
updatedAt: new Date().toISOString(), updatedAt: new Date().toISOString(),
}; };
} }
res.status(201).send({ res.status(201).send({
code: "0", code: "0",
msg: "success", msg: "success",
data: null, data: null,
}); });
}); });
}; };

View File

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

View File

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

View File

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

View File

@@ -1,480 +1,480 @@
import type React from "react"; import type React from "react";
import { useState, useRef, useEffect } from "react"; import { useState, useRef, useEffect } from "react";
import { Card, Input, Button, Badge } from "antd"; import { Card, Input, Button, Badge } from "antd";
import { HomeOutlined } from "@ant-design/icons"; import { HomeOutlined } from "@ant-design/icons";
import { import {
MessageSquare, MessageSquare,
Send, Send,
Bot, Bot,
User, User,
Sparkles, Sparkles,
Database, Database,
BarChart3, BarChart3,
Settings, Settings,
Zap, Zap,
CheckCircle, CheckCircle,
Clock, Clock,
Download, Download,
ArrowLeft, ArrowLeft,
} from "lucide-react"; } from "lucide-react";
import { useNavigate } from "react-router"; import { useNavigate } from "react-router";
import DevelopmentInProgress from "@/components/DevelopmentInProgress"; import DevelopmentInProgress from "@/components/DevelopmentInProgress";
interface Message { interface Message {
id: string; id: string;
type: "user" | "assistant"; type: "user" | "assistant";
content: string; content: string;
timestamp: Date; timestamp: Date;
actions?: Array<{ actions?: Array<{
type: type:
| "create_dataset" | "create_dataset"
| "run_analysis" | "run_analysis"
| "start_synthesis" | "start_synthesis"
| "export_report"; | "export_report";
label: string; label: string;
data?: any; data?: any;
}>; }>;
status?: "pending" | "completed" | "error"; status?: "pending" | "completed" | "error";
} }
interface QuickAction { interface QuickAction {
id: string; id: string;
label: string; label: string;
icon: any; icon: any;
prompt: string; prompt: string;
category: string; category: string;
} }
const quickActions: QuickAction[] = [ const quickActions: QuickAction[] = [
{ {
id: "create_dataset", id: "create_dataset",
label: "创建数据集", label: "创建数据集",
icon: Database, icon: Database,
prompt: "帮我创建一个新的数据集", prompt: "帮我创建一个新的数据集",
category: "数据管理", category: "数据管理",
}, },
{ {
id: "analyze_quality", id: "analyze_quality",
label: "质量分析", label: "质量分析",
icon: BarChart3, icon: BarChart3,
prompt: "分析我的数据集质量", prompt: "分析我的数据集质量",
category: "数据评估", category: "数据评估",
}, },
{ {
id: "start_synthesis", id: "start_synthesis",
label: "数据合成", label: "数据合成",
icon: Sparkles, icon: Sparkles,
prompt: "启动数据合成任务", prompt: "启动数据合成任务",
category: "数据合成", category: "数据合成",
}, },
{ {
id: "process_data", id: "process_data",
label: "数据清洗", label: "数据清洗",
icon: Settings, icon: Settings,
prompt: "对数据集进行预处理", prompt: "对数据集进行预处理",
category: "数据清洗", category: "数据清洗",
}, },
{ {
id: "export_report", id: "export_report",
label: "导出报告", label: "导出报告",
icon: Download, icon: Download,
prompt: "导出最新的分析报告", prompt: "导出最新的分析报告",
category: "报告导出", category: "报告导出",
}, },
{ {
id: "check_status", id: "check_status",
label: "查看状态", label: "查看状态",
icon: Clock, icon: Clock,
prompt: "查看所有任务的运行状态", prompt: "查看所有任务的运行状态",
category: "状态查询", category: "状态查询",
}, },
]; ];
const mockResponses = { const mockResponses = {
: { : {
content: content:
"我来帮您创建一个新的数据集。请告诉我以下信息:\n\n1. 数据集名称\n2. 数据类型(图像、文本、问答对等)\n3. 预期数据量\n4. 数据来源\n\n您也可以直接说出您的需求,我会为您推荐最适合的配置。", "我来帮您创建一个新的数据集。请告诉我以下信息:\n\n1. 数据集名称\n2. 数据类型(图像、文本、问答对等)\n3. 预期数据量\n4. 数据来源\n\n您也可以直接说出您的需求,我会为您推荐最适合的配置。",
actions: [ actions: [
{ type: "create_dataset", label: "开始创建", data: { step: "config" } }, { type: "create_dataset", label: "开始创建", data: { step: "config" } },
], ],
}, },
: { : {
content: 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- 重新标注图文匹配项", "正在为您分析数据集质量...\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: [ actions: [
{ {
type: "run_analysis", type: "run_analysis",
label: "查看详细报告", label: "查看详细报告",
data: { type: "detailed" }, data: { type: "detailed" },
}, },
], ],
}, },
: { : {
content: content:
"我可以帮您启动数据合成任务。目前支持以下合成类型:\n\n🖼️ **图像数据合成**\n- 数据增强(旋转、翻转、亮度调整)\n- 风格迁移\n- GAN生成\n\n📝 **文本数据合成**\n- 同义词替换\n- 回译增强\n- GPT生成\n\n❓ **问答对合成**\n- 基于知识库生成\n- 模板变换\n- 多轮对话生成\n\n请告诉我您需要合成什么类型的数据,以及目标数量。", "我可以帮您启动数据合成任务。目前支持以下合成类型:\n\n🖼️ **图像数据合成**\n- 数据增强(旋转、翻转、亮度调整)\n- 风格迁移\n- GAN生成\n\n📝 **文本数据合成**\n- 同义词替换\n- 回译增强\n- GPT生成\n\n❓ **问答对合成**\n- 基于知识库生成\n- 模板变换\n- 多轮对话生成\n\n请告诉我您需要合成什么类型的数据,以及目标数量。",
actions: [ actions: [
{ {
type: "start_synthesis", type: "start_synthesis",
label: "配置合成任务", label: "配置合成任务",
data: { step: "config" }, data: { step: "config" },
}, },
], ],
}, },
: { : {
content: content:
"正在为您准备最新的分析报告...\n\n📋 **可用报告:**\n- 数据质量评估报告(PDF)\n- 数据分布统计报告(Excel)\n- 模型性能评估报告(PDF)\n- 偏见检测报告(PDF)\n- 综合分析报告(PDF + Excel)\n\n✅ 报告已生成完成,您可以选择下载格式。", "正在为您准备最新的分析报告...\n\n📋 **可用报告:**\n- 数据质量评估报告(PDF)\n- 数据分布统计报告(Excel)\n- 模型性能评估报告(PDF)\n- 偏见检测报告(PDF)\n- 综合分析报告(PDF + Excel)\n\n✅ 报告已生成完成,您可以选择下载格式。",
actions: [ actions: [
{ type: "export_report", label: "下载报告", data: { format: "pdf" } }, { type: "export_report", label: "下载报告", data: { format: "pdf" } },
], ],
}, },
: { : {
content: content:
"📊 **当前任务状态概览:**\n\n🟢 **运行中的任务:**\n- 问答对生成任务:65% 完成\n- 图像质量分析:运行中\n- 知识库构建:等待中\n\n✅ **已完成的任务:**\n- 图像分类数据集创建:已完成\n- PDF文档提取:已完成\n- 训练集配比任务:已完成\n\n⚠️ **需要关注的任务:**\n- 多模态数据合成:暂停(需要用户确认参数)\n\n所有任务运行正常,预计2小时内全部完成。", "📊 **当前任务状态概览:**\n\n🟢 **运行中的任务:**\n- 问答对生成任务:65% 完成\n- 图像质量分析:运行中\n- 知识库构建:等待中\n\n✅ **已完成的任务:**\n- 图像分类数据集创建:已完成\n- PDF文档提取:已完成\n- 训练集配比任务:已完成\n\n⚠️ **需要关注的任务:**\n- 多模态数据合成:暂停(需要用户确认参数)\n\n所有任务运行正常,预计2小时内全部完成。",
actions: [], actions: [],
}, },
}; };
export default function AgentPage() { export default function AgentPage() {
return <DevelopmentInProgress />; return <DevelopmentInProgress />;
const navigate = useNavigate(); const navigate = useNavigate();
const [messages, setMessages] = useState<Message[]>([ const [messages, setMessages] = useState<Message[]>([
{ {
id: "welcome", id: "welcome",
type: "assistant", type: "assistant",
content: content:
"👋 您好!我是 Data Agent,您的AI数据助手。\n\n我可以帮您:\n• 创建和管理数据集\n• 分析数据质量\n• 启动处理任务\n• 生成分析报告\n• 回答数据相关问题\n\n请告诉我您需要什么帮助,或者点击下方的快捷操作开始。", "👋 您好!我是 Data Agent,您的AI数据助手。\n\n我可以帮您:\n• 创建和管理数据集\n• 分析数据质量\n• 启动处理任务\n• 生成分析报告\n• 回答数据相关问题\n\n请告诉我您需要什么帮助,或者点击下方的快捷操作开始。",
timestamp: new Date(), timestamp: new Date(),
}, },
]); ]);
const [inputValue, setInputValue] = useState(""); const [inputValue, setInputValue] = useState("");
const [isTyping, setIsTyping] = useState(false); const [isTyping, setIsTyping] = useState(false);
const messagesEndRef = useRef<HTMLDivElement>(null); const messagesEndRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<any>(null); const inputRef = useRef<any>(null);
const scrollToBottom = () => { const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
}; };
useEffect(() => { useEffect(() => {
scrollToBottom(); scrollToBottom();
}, [messages]); }, [messages]);
const handleSendMessage = async (content: string) => { const handleSendMessage = async (content: string) => {
if (!content.trim()) return; if (!content.trim()) return;
const userMessage: Message = { const userMessage: Message = {
id: Date.now().toString(), id: Date.now().toString(),
type: "user", type: "user",
content: content.trim(), content: content.trim(),
timestamp: new Date(), timestamp: new Date(),
}; };
setMessages((prev) => [...prev, userMessage]); setMessages((prev) => [...prev, userMessage]);
setInputValue(""); setInputValue("");
setIsTyping(true); setIsTyping(true);
// 模拟AI响应 // 模拟AI响应
setTimeout(() => { setTimeout(() => {
const response = generateResponse(content); const response = generateResponse(content);
const assistantMessage: Message = { const assistantMessage: Message = {
id: (Date.now() + 1).toString(), id: (Date.now() + 1).toString(),
type: "assistant", type: "assistant",
content: response.content, content: response.content,
timestamp: new Date(), timestamp: new Date(),
actions: response.actions, actions: response.actions,
}; };
setMessages((prev) => [...prev, assistantMessage]); setMessages((prev) => [...prev, assistantMessage]);
setIsTyping(false); setIsTyping(false);
}, 1500); }, 1500);
}; };
const generateResponse = ( const generateResponse = (
input: string input: string
): { content: string; actions?: any[] } => { ): { content: string; actions?: any[] } => {
const lowerInput = input.toLowerCase(); const lowerInput = input.toLowerCase();
if (lowerInput.includes("创建") && lowerInput.includes("数据集")) { if (lowerInput.includes("创建") && lowerInput.includes("数据集")) {
return mockResponses["创建数据集"]; return mockResponses["创建数据集"];
} else if (lowerInput.includes("质量") || lowerInput.includes("分析")) { } else if (lowerInput.includes("质量") || lowerInput.includes("分析")) {
return mockResponses["质量分析"]; return mockResponses["质量分析"];
} else if (lowerInput.includes("合成") || lowerInput.includes("生成")) { } else if (lowerInput.includes("合成") || lowerInput.includes("生成")) {
return mockResponses["数据合成"]; return mockResponses["数据合成"];
} else if (lowerInput.includes("导出") || lowerInput.includes("报告")) { } else if (lowerInput.includes("导出") || lowerInput.includes("报告")) {
return mockResponses["导出报告"]; return mockResponses["导出报告"];
} else if (lowerInput.includes("状态") || lowerInput.includes("任务")) { } else if (lowerInput.includes("状态") || lowerInput.includes("任务")) {
return mockResponses["查看状态"]; return mockResponses["查看状态"];
} else if (lowerInput.includes("你好") || lowerInput.includes("帮助")) { } else if (lowerInput.includes("你好") || lowerInput.includes("帮助")) {
return { return {
content: content:
"很高兴为您服务!我是专门为数据集管理设计的AI助手。\n\n我的主要能力包括:\n\n🔧 **数据集操作**\n- 创建、导入、导出数据集\n- 数据预处理和清洗\n- 批量操作和自动化\n\n📊 **智能分析**\n- 数据质量评估\n- 分布统计分析\n- 性能和偏见检测\n\n🤖 **AI增强**\n- 智能数据合成\n- 自动标注建议\n- 知识库构建\n\n请告诉我您的具体需求,我会为您提供最合适的解决方案!", "很高兴为您服务!我是专门为数据集管理设计的AI助手。\n\n我的主要能力包括:\n\n🔧 **数据集操作**\n- 创建、导入、导出数据集\n- 数据预处理和清洗\n- 批量操作和自动化\n\n📊 **智能分析**\n- 数据质量评估\n- 分布统计分析\n- 性能和偏见检测\n\n🤖 **AI增强**\n- 智能数据合成\n- 自动标注建议\n- 知识库构建\n\n请告诉我您的具体需求,我会为您提供最合适的解决方案!",
}; };
} else { } else {
return { return {
content: `我理解您想要「${input}」。让我为您分析一下...\n\n基于您的需求,我建议:\n\n1. 首先确认具体的操作目标\n2. 选择合适的数据集和参数\n3. 执行相应的处理流程\n\n您可以提供更多详细信息,或者选择下方的快捷操作来开始。如果需要帮助,请说"帮助"获取完整功能列表。`, content: `我理解您想要「${input}」。让我为您分析一下...\n\n基于您的需求,我建议:\n\n1. 首先确认具体的操作目标\n2. 选择合适的数据集和参数\n3. 执行相应的处理流程\n\n您可以提供更多详细信息,或者选择下方的快捷操作来开始。如果需要帮助,请说"帮助"获取完整功能列表。`,
actions: [ actions: [
{ type: "run_analysis", label: "开始分析", data: { query: input } }, { type: "run_analysis", label: "开始分析", data: { query: input } },
], ],
}; };
} }
}; };
const handleQuickAction = (action: QuickAction) => { const handleQuickAction = (action: QuickAction) => {
handleSendMessage(action.prompt); handleSendMessage(action.prompt);
}; };
const handleActionClick = (action: any) => { const handleActionClick = (action: any) => {
const actionMessage: Message = { const actionMessage: Message = {
id: Date.now().toString(), id: Date.now().toString(),
type: "assistant", type: "assistant",
content: `✅ 正在执行「${action.label}」...\n\n操作已启动,您可以在相应的功能模块中查看详细进度。`, content: `✅ 正在执行「${action.label}」...\n\n操作已启动,您可以在相应的功能模块中查看详细进度。`,
timestamp: new Date(), timestamp: new Date(),
status: "completed", status: "completed",
}; };
setMessages((prev) => [...prev, actionMessage]); setMessages((prev) => [...prev, actionMessage]);
}; };
const handleKeyPress = (e: React.KeyboardEvent<HTMLInputElement>) => { const handleKeyPress = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter" && !e.shiftKey) { if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault(); e.preventDefault();
handleSendMessage(inputValue); handleSendMessage(inputValue);
} }
}; };
const formatMessage = (content: string) => { const formatMessage = (content: string) => {
return content.split("\n").map((line, index) => ( return content.split("\n").map((line, index) => (
<div key={index} className="mb-1"> <div key={index} className="mb-1">
{line || <br />} {line || <br />}
</div> </div>
)); ));
}; };
const onBack = () => { const onBack = () => {
navigate("/"); navigate("/");
}; };
return ( return (
<div className="min-h-screen bg-gradient-to-br from-purple-50 to-pink-50"> <div className="min-h-screen bg-gradient-to-br from-purple-50 to-pink-50">
<div className="h-screen flex flex-col"> <div className="h-screen flex flex-col">
{/* Header */} {/* Header */}
<div className="bg-gradient-to-r from-purple-500 to-pink-500 text-white p-6"> <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="max-w-7xl mx-auto">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<div className="w-12 h-12 bg-white/20 rounded-lg flex items-center justify-center"> <div className="w-12 h-12 bg-white/20 rounded-lg flex items-center justify-center">
<MessageSquare className="w-6 h-6" /> <MessageSquare className="w-6 h-6" />
</div> </div>
<div> <div>
<h1 className="text-2xl font-bold">Data Agent</h1> <h1 className="text-2xl font-bold">Data Agent</h1>
<p className="text-purple-100"> <p className="text-purple-100">
AI驱动的智能数据助手 AI驱动的智能数据助手
</p> </p>
</div> </div>
</div> </div>
<Button <Button
type="default" type="default"
icon={<ArrowLeft className="w-4 h-4 mr-2" />} icon={<ArrowLeft className="w-4 h-4 mr-2" />}
onClick={onBack} onClick={onBack}
className="bg-white/10 border-white/20 text-white hover:bg-white/20 hover:border-white/30" className="bg-white/10 border-white/20 text-white hover:bg-white/20 hover:border-white/30"
> >
</Button> </Button>
</div> </div>
</div> </div>
</div> </div>
<div className="flex-1 max-w-7xl mx-auto h-full w-full p-6"> <div className="flex-1 max-w-7xl mx-auto h-full w-full p-6">
<div className="h-full flex gap-6"> <div className="h-full flex gap-6">
{/* Chat Area */} {/* Chat Area */}
<div className="lg:col-span-3 flex flex-1 flex-col h-full"> <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="flex-1 flex flex-col h-full shadow-lg">
<div className="pb-3 bg-white rounded-t-lg"> <div className="pb-3 bg-white rounded-t-lg">
<div className="flex items-center justify-between p-4"> <div className="flex items-center justify-between p-4">
<span className="text-lg font-semibold"></span> <span className="text-lg font-semibold"></span>
<div> <div>
<span className="w-2 h-2 bg-green-500 rounded-full mr-1 inline-block" /> <span className="w-2 h-2 bg-green-500 rounded-full mr-1 inline-block" />
线 线
</div> </div>
</div> </div>
</div> </div>
<div className="flex-1 flex flex-col justify-between h-full p-0 min-h-0"> <div className="flex-1 flex flex-col justify-between h-full p-0 min-h-0">
{/* Messages */} {/* Messages */}
<div className="flex-1 overflow-y-auto p-6 bg-white"> <div className="flex-1 overflow-y-auto p-6 bg-white">
<div className="space-y-4 pb-4"> <div className="space-y-4 pb-4">
{messages.map((message) => ( {messages.map((message) => (
<div <div
key={message.id} key={message.id}
className={`flex gap-3 ${ className={`flex gap-3 ${
message.type === "user" message.type === "user"
? "justify-end" ? "justify-end"
: "justify-start" : "justify-start"
}`} }`}
> >
{message.type === "assistant" && ( {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"> <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" /> <Bot className="w-4 h-4 text-white" />
</div> </div>
)} )}
<div <div
className={`max-w-[80%] rounded-lg px-4 py-3 ${ className={`max-w-[80%] rounded-lg px-4 py-3 ${
message.type === "user" message.type === "user"
? "bg-blue-500 text-white" ? "bg-blue-500 text-white"
: "bg-white text-gray-900 shadow-sm border border-gray-100" : "bg-white text-gray-900 shadow-sm border border-gray-100"
}`} }`}
> >
<div className="text-sm whitespace-pre-wrap"> <div className="text-sm whitespace-pre-wrap">
{formatMessage(message.content)} {formatMessage(message.content)}
</div> </div>
{message.actions && message.actions.length > 0 && ( {message.actions && message.actions.length > 0 && (
<div className="mt-3 space-y-2"> <div className="mt-3 space-y-2">
{message.actions.map((action, index) => ( {message.actions.map((action, index) => (
<Button <Button
key={index} key={index}
type="default" type="default"
size="small" size="small"
className="mr-2 mb-2" className="mr-2 mb-2"
onClick={() => handleActionClick(action)} onClick={() => handleActionClick(action)}
> >
{action.label} {action.label}
</Button> </Button>
))} ))}
</div> </div>
)} )}
<div className="text-xs opacity-70 mt-2"> <div className="text-xs opacity-70 mt-2">
{message.timestamp.toLocaleTimeString()} {message.timestamp.toLocaleTimeString()}
</div> </div>
</div> </div>
{message.type === "user" && ( {message.type === "user" && (
<div className="w-8 h-8 bg-blue-500 rounded-full flex items-center justify-center flex-shrink-0"> <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" /> <User className="w-4 h-4 text-white" />
</div> </div>
)} )}
</div> </div>
))} ))}
{isTyping && ( {isTyping && (
<div className="flex gap-3 justify-start"> <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"> <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" /> <Bot className="w-4 h-4 text-white" />
</div> </div>
<div className="bg-white rounded-lg px-4 py-3 shadow-sm border border-gray-100"> <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="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"></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-100"></div>
<div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce delay-200"></div> <div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce delay-200"></div>
</div> </div>
</div> </div>
</div> </div>
)} )}
</div> </div>
<div ref={messagesEndRef} /> <div ref={messagesEndRef} />
</div> </div>
{/* Input Area */} {/* Input Area */}
<div className="border-t border-gray-200 p-4 bg-white rounded-b-lg"> <div className="border-t border-gray-200 p-4 bg-white rounded-b-lg">
<div className="flex gap-2"> <div className="flex gap-2">
<Input <Input
ref={inputRef} ref={inputRef}
value={inputValue} value={inputValue}
onChange={(e) => setInputValue(e.target.value)} onChange={(e) => setInputValue(e.target.value)}
onKeyDown={handleKeyPress} onKeyDown={handleKeyPress}
placeholder="输入您的需求,例如:创建一个图像分类数据集..." placeholder="输入您的需求,例如:创建一个图像分类数据集..."
disabled={isTyping} disabled={isTyping}
/> />
<Button <Button
type="primary" type="primary"
onClick={() => handleSendMessage(inputValue)} onClick={() => handleSendMessage(inputValue)}
disabled={!inputValue.trim() || isTyping} 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" 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" /> <Send className="w-4 h-4" />
</Button> </Button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{/* Quick Actions Sidebar */} {/* Quick Actions Sidebar */}
<div className="w-72 flex flex-col gap-6"> <div className="w-72 flex flex-col gap-6">
<Card className="shadow-lg"> <Card className="shadow-lg">
<div className=""> <div className="">
<span className="text-lg font-semibold"></span> <span className="text-lg font-semibold"></span>
<div className="text-sm text-gray-500"> <div className="text-sm text-gray-500">
</div> </div>
</div> </div>
<div className="space-y-2 p-4"> <div className="space-y-2 p-4">
{quickActions.map((action) => ( {quickActions.map((action) => (
<Button <Button
key={action.id} key={action.id}
type="default" type="default"
className="w-full justify-start h-auto p-3 text-left" className="w-full justify-start h-auto p-3 text-left"
onClick={() => handleQuickAction(action)} onClick={() => handleQuickAction(action)}
> >
<action.icon className="w-4 h-4 mr-2 flex-shrink-0" /> <action.icon className="w-4 h-4 mr-2 flex-shrink-0" />
<div className="text-left"> <div className="text-left">
<div className="font-medium text-sm"> <div className="font-medium text-sm">
{action.label} {action.label}
</div> </div>
</div> </div>
</Button> </Button>
))} ))}
</div> </div>
</Card> </Card>
<Card className="shadow-lg"> <Card className="shadow-lg">
<div className="pb-3"> <div className="pb-3">
<span className="text-lg font-semibold"></span> <span className="text-lg font-semibold"></span>
</div> </div>
<div className="space-y-3 p-4 pt-0"> <div className="space-y-3 p-4 pt-0">
<div className="flex items-center gap-2 text-sm"> <div className="flex items-center gap-2 text-sm">
<CheckCircle className="w-4 h-4 text-green-500" /> <CheckCircle className="w-4 h-4 text-green-500" />
<span>AI服务正常</span> <span>AI服务正常</span>
</div> </div>
<div className="flex items-center gap-2 text-sm"> <div className="flex items-center gap-2 text-sm">
<Clock className="w-4 h-4 text-blue-500" /> <Clock className="w-4 h-4 text-blue-500" />
<span>3</span> <span>3</span>
</div> </div>
<div className="flex items-center gap-2 text-sm"> <div className="flex items-center gap-2 text-sm">
<Database className="w-4 h-4 text-purple-500" /> <Database className="w-4 h-4 text-purple-500" />
<span>12</span> <span>12</span>
</div> </div>
<div className="flex items-center gap-2 text-sm"> <div className="flex items-center gap-2 text-sm">
<Zap className="w-4 h-4 text-orange-500" /> <Zap className="w-4 h-4 text-orange-500" />
<span>响应时间: 0.8s</span> <span>响应时间: 0.8s</span>
</div> </div>
</div> </div>
</Card> </Card>
<Card className="shadow-lg"> <Card className="shadow-lg">
<div className="pb-3"> <div className="pb-3">
<span className="text-lg font-semibold">使</span> <span className="text-lg font-semibold">使</span>
</div> </div>
<div className="space-y-2 text-sm text-gray-600 p-4 pt-0"> <div className="space-y-2 text-sm text-gray-600 p-4 pt-0">
<div>💡 </div> <div>💡 </div>
<div>🔍 </div> <div>🔍 </div>
<div>📊 </div> <div>📊 </div>
<div> 使</div> <div> 使</div>
</div> </div>
</Card> </Card>
<Card className="shadow-lg"> <Card className="shadow-lg">
<div className="pt-6 p-4"> <div className="pt-6 p-4">
<Button <Button
type="default" type="default"
className="w-full" className="w-full"
icon={<HomeOutlined />} icon={<HomeOutlined />}
onClick={onBack} onClick={onBack}
> >
</Button> </Button>
</div> </div>
</Card> </Card>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
); );
} }

View File

@@ -1,229 +1,229 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Card, message } from "antd"; import { Card, message } from "antd";
import { Button, Badge, Progress, Checkbox } from "antd"; import { Button, Badge, Progress, Checkbox } from "antd";
import { import {
ArrowLeft, ArrowLeft,
FileText, FileText,
ImageIcon, ImageIcon,
Video, Video,
Music, Music,
Save, Save,
SkipForward, SkipForward,
CheckCircle, CheckCircle,
Eye, Eye,
Settings, Settings,
} from "lucide-react"; } from "lucide-react";
import { mockTasks } from "@/mock/annotation"; import { mockTasks } from "@/mock/annotation";
import { Outlet, useNavigate } from "react-router"; import { Outlet, useNavigate } from "react-router";
export default function AnnotationWorkspace() { export default function AnnotationWorkspace() {
const navigate = useNavigate(); const navigate = useNavigate();
const [task, setTask] = useState(mockTasks[0]); const [task, setTask] = useState(mockTasks[0]);
const [currentFileIndex, setCurrentFileIndex] = useState(0); const [currentFileIndex, setCurrentFileIndex] = useState(0);
const [annotationProgress, setAnnotationProgress] = useState({ const [annotationProgress, setAnnotationProgress] = useState({
completed: task.completedCount, completed: task.completedCount,
skipped: task.skippedCount, skipped: task.skippedCount,
total: task.totalCount, total: task.totalCount,
}); });
const handleSaveAndNext = () => { const handleSaveAndNext = () => {
setAnnotationProgress((prev) => ({ setAnnotationProgress((prev) => ({
...prev, ...prev,
completed: prev.completed + 1, completed: prev.completed + 1,
})); }));
if (currentFileIndex < task.totalCount - 1) { if (currentFileIndex < task.totalCount - 1) {
setCurrentFileIndex(currentFileIndex + 1); setCurrentFileIndex(currentFileIndex + 1);
} }
message({ message({
title: "标注已保存", title: "标注已保存",
description: "标注结果已保存,自动跳转到下一个", description: "标注结果已保存,自动跳转到下一个",
}); });
}; };
const handleSkipAndNext = () => { const handleSkipAndNext = () => {
setAnnotationProgress((prev) => ({ setAnnotationProgress((prev) => ({
...prev, ...prev,
skipped: prev.skipped + 1, skipped: prev.skipped + 1,
})); }));
if (currentFileIndex < task.totalCount - 1) { if (currentFileIndex < task.totalCount - 1) {
setCurrentFileIndex(currentFileIndex + 1); setCurrentFileIndex(currentFileIndex + 1);
} }
message({ message({
title: "已跳过", title: "已跳过",
description: "已跳过当前项目,自动跳转到下一个", description: "已跳过当前项目,自动跳转到下一个",
}); });
}; };
const getDatasetTypeIcon = (type: string) => { const getDatasetTypeIcon = (type: string) => {
switch (type) { switch (type) {
case "text": case "text":
return <FileText className="w-4 h-4 text-blue-500" />; return <FileText className="w-4 h-4 text-blue-500" />;
case "image": case "image":
return <ImageIcon className="w-4 h-4 text-green-500" />; return <ImageIcon className="w-4 h-4 text-green-500" />;
case "video": case "video":
return <Video className="w-4 h-4 text-purple-500" />; return <Video className="w-4 h-4 text-purple-500" />;
case "audio": case "audio":
return <Music className="w-4 h-4 text-orange-500" />; return <Music className="w-4 h-4 text-orange-500" />;
default: default:
return <FileText className="w-4 h-4 text-gray-500" />; return <FileText className="w-4 h-4 text-gray-500" />;
} }
}; };
const currentProgress = Math.round( const currentProgress = Math.round(
((annotationProgress.completed + annotationProgress.skipped) / ((annotationProgress.completed + annotationProgress.skipped) /
annotationProgress.total) * annotationProgress.total) *
100 100
); );
return ( return (
<div className="h-screen flex flex-col"> <div className="h-screen flex flex-col">
{/* Header */} {/* Header */}
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
<div className="flex items-center"> <div className="flex items-center">
<Button <Button
type="text" type="text"
onClick={() => navigate("/data/annotation")} onClick={() => navigate("/data/annotation")}
icon={<ArrowLeft className="w-4 h-4" />} icon={<ArrowLeft className="w-4 h-4" />}
></Button> ></Button>
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
{getDatasetTypeIcon(task.datasetType)} {getDatasetTypeIcon(task.datasetType)}
<span className="text-xl font-bold">{task.name}</span> <span className="text-xl font-bold">{task.name}</span>
</div> </div>
</div> </div>
<div className="flex items-center space-x-4"> <div className="flex items-center space-x-4">
<div className="text-sm text-gray-600"> <div className="text-sm text-gray-600">
{currentFileIndex + 1} / {task.totalCount} {currentFileIndex + 1} / {task.totalCount}
</div> </div>
<div className="flex items-center space-x-2 min-w-56"> <div className="flex items-center space-x-2 min-w-56">
<span className="text-sm text-gray-600">:</span> <span className="text-sm text-gray-600">:</span>
<Progress <Progress
percent={currentProgress} percent={currentProgress}
showInfo={false} showInfo={false}
className="w-24 h-2" className="w-24 h-2"
/> />
<span className="text-sm font-medium">{currentProgress}%</span> <span className="text-sm font-medium">{currentProgress}%</span>
</div> </div>
</div> </div>
</div> </div>
{/* Main Content */} {/* Main Content */}
<div className="bg-white border-b border-gray-200 px-6 py-4 flex-1 flex"> <div className="bg-white border-b border-gray-200 px-6 py-4 flex-1 flex">
{/* Annotation Area */} {/* Annotation Area */}
<div className="flex-1 flex flex-col"> <div className="flex-1 flex flex-col">
<Outlet /> <Outlet />
</div> </div>
{/* Right Sidebar - Only show for text and image types */} {/* Right Sidebar - Only show for text and image types */}
{(task.datasetType === "text" || task.datasetType === "image") && ( {(task.datasetType === "text" || task.datasetType === "image") && (
<div className="w-80 border-l border-gray-200 p-4 space-y-4"> <div className="w-80 border-l border-gray-200 p-4 space-y-4">
{/* Progress Stats */} {/* Progress Stats */}
<Card> <Card>
<div className="space-y-3 pt-4"> <div className="space-y-3 pt-4">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<span className="text-sm text-gray-600"></span> <span className="text-sm text-gray-600"></span>
<span className="font-medium text-green-500"> <span className="font-medium text-green-500">
{annotationProgress.completed} {annotationProgress.completed}
</span> </span>
</div> </div>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<span className="text-sm text-gray-600"></span> <span className="text-sm text-gray-600"></span>
<span className="font-medium text-red-500"> <span className="font-medium text-red-500">
{annotationProgress.skipped} {annotationProgress.skipped}
</span> </span>
</div> </div>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<span className="text-sm text-gray-600"></span> <span className="text-sm text-gray-600"></span>
<span className="font-medium text-gray-600"> <span className="font-medium text-gray-600">
{annotationProgress.total - {annotationProgress.total -
annotationProgress.completed - annotationProgress.completed -
annotationProgress.skipped} annotationProgress.skipped}
</span> </span>
</div> </div>
<div className="border-t border-gray-200 my-3" /> <div className="border-t border-gray-200 my-3" />
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<span className="text-sm text-gray-600"></span> <span className="text-sm text-gray-600"></span>
<span className="font-medium">{currentProgress}%</span> <span className="font-medium">{currentProgress}%</span>
</div> </div>
</div> </div>
</Card> </Card>
{/* Quick Actions */} {/* Quick Actions */}
<Card> <Card>
<div className="pt-4 space-y-2"> <div className="pt-4 space-y-2">
<Button <Button
type="primary" type="primary"
block block
onClick={handleSaveAndNext} onClick={handleSaveAndNext}
className="bg-green-500 border-green-500 hover:bg-green-600 hover:border-green-600" className="bg-green-500 border-green-500 hover:bg-green-600 hover:border-green-600"
icon={<CheckCircle className="w-4 h-4 mr-2" />} icon={<CheckCircle className="w-4 h-4 mr-2" />}
> >
</Button> </Button>
<Button <Button
block block
onClick={handleSkipAndNext} onClick={handleSkipAndNext}
icon={<SkipForward className="w-4 h-4 mr-2" />} icon={<SkipForward className="w-4 h-4 mr-2" />}
> >
</Button> </Button>
<Button block icon={<Save className="w-4 h-4 mr-2" />}> <Button block icon={<Save className="w-4 h-4 mr-2" />}>
</Button> </Button>
<Button block icon={<Eye className="w-4 h-4 mr-2" />}> <Button block icon={<Eye className="w-4 h-4 mr-2" />}>
</Button> </Button>
</div> </div>
</Card> </Card>
{/* Navigation */} {/* Navigation */}
<Card> <Card>
<div className="pt-4 space-y-2"> <div className="pt-4 space-y-2">
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<Button <Button
block block
disabled={currentFileIndex === 0} disabled={currentFileIndex === 0}
onClick={() => setCurrentFileIndex(currentFileIndex - 1)} onClick={() => setCurrentFileIndex(currentFileIndex - 1)}
> >
</Button> </Button>
<Button <Button
block block
disabled={currentFileIndex === task.totalCount - 1} disabled={currentFileIndex === task.totalCount - 1}
onClick={() => setCurrentFileIndex(currentFileIndex + 1)} onClick={() => setCurrentFileIndex(currentFileIndex + 1)}
> >
</Button> </Button>
</div> </div>
<div className="text-xs text-gray-500"> <div className="text-xs text-gray-500">
: {currentFileIndex + 1} / {task.totalCount} : {currentFileIndex + 1} / {task.totalCount}
</div> </div>
</div> </div>
</Card> </Card>
{/* Settings */} {/* Settings */}
<Card> <Card>
<div className="pt-4 space-y-3"> <div className="pt-4 space-y-3">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<span className="text-sm"></span> <span className="text-sm"></span>
<Checkbox defaultChecked /> <Checkbox defaultChecked />
</div> </div>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<span className="text-sm"></span> <span className="text-sm"></span>
<Checkbox defaultChecked /> <Checkbox defaultChecked />
</div> </div>
<Button block icon={<Settings className="w-4 h-4 mr-2" />}> <Button block icon={<Settings className="w-4 h-4 mr-2" />}>
</Button> </Button>
</div> </div>
</Card> </Card>
</div> </div>
)} )}
</div> </div>
</div> </div>
); );
} }

View File

@@ -1,457 +1,457 @@
import { useState } from "react"; import { useState } from "react";
import { Card, Button, Badge, Input, Checkbox } from "antd"; import { Card, Button, Badge, Input, Checkbox } from "antd";
import { import {
File, File,
Search, Search,
CheckCircle, CheckCircle,
ThumbsUp, ThumbsUp,
ThumbsDown, ThumbsDown,
MessageSquare, MessageSquare,
HelpCircle, HelpCircle,
} from "lucide-react"; } from "lucide-react";
interface QAPair { interface QAPair {
id: string; id: string;
question: string; question: string;
answer: string; answer: string;
status: "pending" | "approved" | "rejected"; status: "pending" | "approved" | "rejected";
confidence?: number; confidence?: number;
} }
interface FileData { interface FileData {
id: string; id: string;
name: string; name: string;
qaPairs: QAPair[]; qaPairs: QAPair[];
} }
interface TextAnnotationWorkspaceProps { interface TextAnnotationWorkspaceProps {
task: any; task: any;
currentFileIndex: number; currentFileIndex: number;
onSaveAndNext: () => void; onSaveAndNext: () => void;
onSkipAndNext: () => void; onSkipAndNext: () => void;
} }
// 模拟文件数据 // 模拟文件数据
const mockFiles: FileData[] = [ const mockFiles: FileData[] = [
{ {
id: "1", id: "1",
name: "document_001.txt", name: "document_001.txt",
qaPairs: [ qaPairs: [
{ {
id: "1", id: "1",
question: "什么是人工智能?", question: "什么是人工智能?",
answer: answer:
"人工智能(AI)是计算机科学的一个分支,致力于创建能够执行通常需要人类智能的任务的系统。", "人工智能(AI)是计算机科学的一个分支,致力于创建能够执行通常需要人类智能的任务的系统。",
status: "pending", status: "pending",
confidence: 0.85, confidence: 0.85,
}, },
{ {
id: "2", id: "2",
question: "机器学习和深度学习有什么区别?", question: "机器学习和深度学习有什么区别?",
answer: answer:
"机器学习是人工智能的一个子集,而深度学习是机器学习的一个子集。深度学习使用神经网络来模拟人脑的工作方式。", "机器学习是人工智能的一个子集,而深度学习是机器学习的一个子集。深度学习使用神经网络来模拟人脑的工作方式。",
status: "pending", status: "pending",
confidence: 0.92, confidence: 0.92,
}, },
{ {
id: "3", id: "3",
question: "什么是神经网络?", question: "什么是神经网络?",
answer: answer:
"神经网络是一种受生物神经网络启发的计算模型,由相互连接的节点(神经元)组成,能够学习和识别模式。", "神经网络是一种受生物神经网络启发的计算模型,由相互连接的节点(神经元)组成,能够学习和识别模式。",
status: "pending", status: "pending",
confidence: 0.78, confidence: 0.78,
}, },
], ],
}, },
{ {
id: "2", id: "2",
name: "document_002.txt", name: "document_002.txt",
qaPairs: [ qaPairs: [
{ {
id: "4", id: "4",
question: "什么是自然语言处理?", question: "什么是自然语言处理?",
answer: answer:
"自然语言处理(NLP)是人工智能的一个分支,专注于使计算机能够理解、解释和生成人类语言。", "自然语言处理(NLP)是人工智能的一个分支,专注于使计算机能够理解、解释和生成人类语言。",
status: "pending", status: "pending",
confidence: 0.88, confidence: 0.88,
}, },
{ {
id: "5", id: "5",
question: "计算机视觉的应用有哪些?", question: "计算机视觉的应用有哪些?",
answer: answer:
"计算机视觉广泛应用于图像识别、人脸识别、自动驾驶、医学影像分析、安防监控等领域。", "计算机视觉广泛应用于图像识别、人脸识别、自动驾驶、医学影像分析、安防监控等领域。",
status: "pending", status: "pending",
confidence: 0.91, confidence: 0.91,
}, },
], ],
}, },
]; ];
export default function TextAnnotationWorkspace({ export default function TextAnnotationWorkspace({
onSaveAndNext, onSaveAndNext,
onSkipAndNext, onSkipAndNext,
}: TextAnnotationWorkspaceProps) { }: TextAnnotationWorkspaceProps) {
const [selectedFile, setSelectedFile] = useState<FileData | null>( const [selectedFile, setSelectedFile] = useState<FileData | null>(
mockFiles[0] mockFiles[0]
); );
const [searchQuery, setSearchQuery] = useState(""); const [searchQuery, setSearchQuery] = useState("");
const [statusFilter, setStatusFilter] = useState("all"); const [statusFilter, setStatusFilter] = useState("all");
const [selectedQAs, setSelectedQAs] = useState<string[]>([]); const [selectedQAs, setSelectedQAs] = useState<string[]>([]);
const handleFileSelect = (file: FileData) => { const handleFileSelect = (file: FileData) => {
setSelectedFile(file); setSelectedFile(file);
setSelectedQAs([]); setSelectedQAs([]);
}; };
const handleQAStatusChange = ( const handleQAStatusChange = (
qaId: string, qaId: string,
status: "approved" | "rejected" status: "approved" | "rejected"
) => { ) => {
if (selectedFile) { if (selectedFile) {
const updatedFile = { const updatedFile = {
...selectedFile, ...selectedFile,
qaPairs: selectedFile.qaPairs.map((qa) => qaPairs: selectedFile.qaPairs.map((qa) =>
qa.id === qaId ? { ...qa, status } : qa qa.id === qaId ? { ...qa, status } : qa
), ),
}; };
setSelectedFile(updatedFile); setSelectedFile(updatedFile);
message({ message({
title: status === "approved" ? "已标记为留用" : "已标记为不留用", title: status === "approved" ? "已标记为留用" : "已标记为不留用",
description: `QA对 "${qaId}" 状态已更新`, description: `QA对 "${qaId}" 状态已更新`,
}); });
} }
}; };
const handleBatchApprove = () => { const handleBatchApprove = () => {
if (selectedFile && selectedQAs.length > 0) { if (selectedFile && selectedQAs.length > 0) {
const updatedFile = { const updatedFile = {
...selectedFile, ...selectedFile,
qaPairs: selectedFile.qaPairs.map((qa) => qaPairs: selectedFile.qaPairs.map((qa) =>
selectedQAs.includes(qa.id) selectedQAs.includes(qa.id)
? { ...qa, status: "approved" as const } ? { ...qa, status: "approved" as const }
: qa : qa
), ),
}; };
setSelectedFile(updatedFile); setSelectedFile(updatedFile);
setSelectedQAs([]); setSelectedQAs([]);
message({ message({
title: "批量操作完成", title: "批量操作完成",
description: `已将 ${selectedQAs.length} 个QA对标记为留用`, description: `已将 ${selectedQAs.length} 个QA对标记为留用`,
}); });
} }
}; };
const handleBatchReject = () => { const handleBatchReject = () => {
if (selectedFile && selectedQAs.length > 0) { if (selectedFile && selectedQAs.length > 0) {
const updatedFile = { const updatedFile = {
...selectedFile, ...selectedFile,
qaPairs: selectedFile.qaPairs.map((qa) => qaPairs: selectedFile.qaPairs.map((qa) =>
selectedQAs.includes(qa.id) selectedQAs.includes(qa.id)
? { ...qa, status: "rejected" as const } ? { ...qa, status: "rejected" as const }
: qa : qa
), ),
}; };
setSelectedFile(updatedFile); setSelectedFile(updatedFile);
setSelectedQAs([]); setSelectedQAs([]);
message({ message({
title: "批量操作完成", title: "批量操作完成",
description: `已将 ${selectedQAs.length} 个QA对标记为不留用`, description: `已将 ${selectedQAs.length} 个QA对标记为不留用`,
}); });
} }
}; };
const handleQASelect = (qaId: string, checked: boolean) => { const handleQASelect = (qaId: string, checked: boolean) => {
if (checked) { if (checked) {
setSelectedQAs([...selectedQAs, qaId]); setSelectedQAs([...selectedQAs, qaId]);
} else { } else {
setSelectedQAs(selectedQAs.filter((id) => id !== qaId)); setSelectedQAs(selectedQAs.filter((id) => id !== qaId));
} }
}; };
const handleSelectAll = (checked: boolean) => { const handleSelectAll = (checked: boolean) => {
if (checked && selectedFile) { if (checked && selectedFile) {
setSelectedQAs(selectedFile.qaPairs.map((qa) => qa.id)); setSelectedQAs(selectedFile.qaPairs.map((qa) => qa.id));
} else { } else {
setSelectedQAs([]); setSelectedQAs([]);
} }
}; };
const getStatusBadge = (status: string) => { const getStatusBadge = (status: string) => {
switch (status) { switch (status) {
case "approved": case "approved":
return <Badge className="bg-green-100 text-green-800"></Badge>; return <Badge className="bg-green-100 text-green-800"></Badge>;
case "rejected": case "rejected":
return <Badge className="bg-red-100 text-red-800"></Badge>; return <Badge className="bg-red-100 text-red-800"></Badge>;
default: default:
return <Badge></Badge>; return <Badge></Badge>;
} }
}; };
const getConfidenceColor = (confidence?: number) => { const getConfidenceColor = (confidence?: number) => {
if (!confidence) return "text-gray-500"; if (!confidence) return "text-gray-500";
if (confidence >= 0.8) return "text-green-600"; if (confidence >= 0.8) return "text-green-600";
if (confidence >= 0.6) return "text-yellow-600"; if (confidence >= 0.6) return "text-yellow-600";
return "text-red-600"; return "text-red-600";
}; };
const filteredQAs = const filteredQAs =
selectedFile?.qaPairs.filter((qa) => { selectedFile?.qaPairs.filter((qa) => {
const matchesSearch = const matchesSearch =
qa.question.toLowerCase().includes(searchQuery.toLowerCase()) || qa.question.toLowerCase().includes(searchQuery.toLowerCase()) ||
qa.answer.toLowerCase().includes(searchQuery.toLowerCase()); qa.answer.toLowerCase().includes(searchQuery.toLowerCase());
const matchesStatus = const matchesStatus =
statusFilter === "all" || qa.status === statusFilter; statusFilter === "all" || qa.status === statusFilter;
return matchesSearch && matchesStatus; return matchesSearch && matchesStatus;
}) || []; }) || [];
return ( return (
<div className="flex-1 flex"> <div className="flex-1 flex">
{/* File List */} {/* File List */}
<div className="w-80 border-r bg-gray-50 p-4"> <div className="w-80 border-r bg-gray-50 p-4">
<div className="space-y-4"> <div className="space-y-4">
<div> <div>
<h3 className="font-medium mb-2"></h3> <h3 className="font-medium mb-2"></h3>
<div className="relative"> <div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" /> <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" /> <Input placeholder="搜索文件..." className="pl-10" />
</div> </div>
</div> </div>
<div className="h-96"> <div className="h-96">
<div className="space-y-2"> <div className="space-y-2">
{mockFiles.map((file) => ( {mockFiles.map((file) => (
<div <div
key={file.id} key={file.id}
className={`p-3 border rounded-lg cursor-pointer transition-colors ${ className={`p-3 border rounded-lg cursor-pointer transition-colors ${
selectedFile?.id === file.id selectedFile?.id === file.id
? "border-blue-500 bg-blue-50" ? "border-blue-500 bg-blue-50"
: "border-gray-200 hover:border-gray-300" : "border-gray-200 hover:border-gray-300"
}`} }`}
onClick={() => handleFileSelect(file)} onClick={() => handleFileSelect(file)}
> >
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<File className="w-4 h-4 text-gray-400" /> <File className="w-4 h-4 text-gray-400" />
<div className="flex-1"> <div className="flex-1">
<div className="font-medium text-sm">{file.name}</div> <div className="font-medium text-sm">{file.name}</div>
<div className="text-xs text-gray-500"> <div className="text-xs text-gray-500">
{file.qaPairs.length} QA对 {file.qaPairs.length} QA对
</div> </div>
</div> </div>
</div> </div>
</div> </div>
))} ))}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{/* QA Annotation Area */} {/* QA Annotation Area */}
<div className="flex-1 p-6"> <div className="flex-1 p-6">
{selectedFile ? ( {selectedFile ? (
<div className="space-y-4"> <div className="space-y-4">
{/* Header */} {/* Header */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h2 className="text-xl font-bold">{selectedFile.name}</h2> <h2 className="text-xl font-bold">{selectedFile.name}</h2>
<p className="text-gray-500"> <p className="text-gray-500">
{selectedFile.qaPairs.length} QA对 {selectedFile.qaPairs.length} QA对
</p> </p>
</div> </div>
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<Button <Button
onClick={onSaveAndNext} onClick={onSaveAndNext}
className="bg-green-600 hover:bg-green-700" className="bg-green-600 hover:bg-green-700"
> >
<CheckCircle className="w-4 h-4 mr-2" /> <CheckCircle className="w-4 h-4 mr-2" />
</Button> </Button>
<Button onClick={onSkipAndNext}></Button> <Button onClick={onSkipAndNext}></Button>
</div> </div>
</div> </div>
{/* Filters and Batch Actions */} {/* Filters and Batch Actions */}
<Card> <Card>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center space-x-4"> <div className="flex items-center space-x-4">
<div className="relative"> <div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" /> <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
<Input <Input
placeholder="搜索QA对..." placeholder="搜索QA对..."
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}
className="pl-10 w-64" className="pl-10 w-64"
/> />
</div> </div>
<select <select
value={statusFilter} value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value)} onChange={(e) => setStatusFilter(e.target.value)}
className="px-3 py-2 border rounded-md text-sm" className="px-3 py-2 border rounded-md text-sm"
> >
<option value="all"></option> <option value="all"></option>
<option value="pending"></option> <option value="pending"></option>
<option value="approved"></option> <option value="approved"></option>
<option value="rejected"></option> <option value="rejected"></option>
</select> </select>
</div> </div>
{selectedQAs.length > 0 && ( {selectedQAs.length > 0 && (
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<span className="text-sm text-gray-600"> <span className="text-sm text-gray-600">
{selectedQAs.length} {selectedQAs.length}
</span> </span>
<Button <Button
onClick={handleBatchApprove} onClick={handleBatchApprove}
size="sm" size="sm"
className="bg-green-600 hover:bg-green-700" className="bg-green-600 hover:bg-green-700"
> >
<ThumbsUp className="w-4 h-4 mr-1" /> <ThumbsUp className="w-4 h-4 mr-1" />
</Button> </Button>
<Button <Button
onClick={handleBatchReject} onClick={handleBatchReject}
size="sm" size="sm"
variant="destructive" variant="destructive"
> >
<ThumbsDown className="w-4 h-4 mr-1" /> <ThumbsDown className="w-4 h-4 mr-1" />
</Button> </Button>
</div> </div>
)} )}
</div> </div>
</Card> </Card>
{/* QA List */} {/* QA List */}
<div className="space-y-4"> <div className="space-y-4">
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<Checkbox <Checkbox
checked={ checked={
selectedQAs.length === filteredQAs.length && selectedQAs.length === filteredQAs.length &&
filteredQAs.length > 0 filteredQAs.length > 0
} }
onChange={handleSelectAll} onChange={handleSelectAll}
/> />
<span className="text-sm font-medium"></span> <span className="text-sm font-medium"></span>
</div> </div>
<div className="h-500"> <div className="h-500">
<div className="space-y-4"> <div className="space-y-4">
{filteredQAs.map((qa) => ( {filteredQAs.map((qa) => (
<Card <Card
key={qa.id} key={qa.id}
className="hover:shadow-md transition-shadow" className="hover:shadow-md transition-shadow"
> >
<div className="space-y-3"> <div className="space-y-3">
<div className="flex items-start justify-between"> <div className="flex items-start justify-between">
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<Checkbox <Checkbox
checked={selectedQAs.includes(qa.id)} checked={selectedQAs.includes(qa.id)}
onCheckedChange={(checked) => onCheckedChange={(checked) =>
handleQASelect(qa.id, checked as boolean) handleQASelect(qa.id, checked as boolean)
} }
/> />
<MessageSquare className="w-4 h-4 text-blue-500" /> <MessageSquare className="w-4 h-4 text-blue-500" />
<span className="text-sm font-medium"> <span className="text-sm font-medium">
QA-{qa.id} QA-{qa.id}
</span> </span>
</div> </div>
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
{qa.confidence && ( {qa.confidence && (
<span <span
className={`text-xs ${getConfidenceColor( className={`text-xs ${getConfidenceColor(
qa.confidence qa.confidence
)}`} )}`}
> >
: {(qa.confidence * 100).toFixed(1)}% : {(qa.confidence * 100).toFixed(1)}%
</span> </span>
)} )}
{getStatusBadge(qa.status)} {getStatusBadge(qa.status)}
</div> </div>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<div> <div>
<div className="flex items-center space-x-2 mb-1"> <div className="flex items-center space-x-2 mb-1">
<HelpCircle className="w-4 h-4 text-blue-500" /> <HelpCircle className="w-4 h-4 text-blue-500" />
<span className="text-sm font-medium text-blue-700"> <span className="text-sm font-medium text-blue-700">
</span> </span>
</div> </div>
<p className="text-sm bg-blue-50 p-3 rounded"> <p className="text-sm bg-blue-50 p-3 rounded">
{qa.question} {qa.question}
</p> </p>
</div> </div>
<div> <div>
<div className="flex items-center space-x-2 mb-1"> <div className="flex items-center space-x-2 mb-1">
<MessageSquare className="w-4 h-4 text-green-500" /> <MessageSquare className="w-4 h-4 text-green-500" />
<span className="text-sm font-medium text-green-700"> <span className="text-sm font-medium text-green-700">
</span> </span>
</div> </div>
<p className="text-sm bg-green-50 p-3 rounded"> <p className="text-sm bg-green-50 p-3 rounded">
{qa.answer} {qa.answer}
</p> </p>
</div> </div>
</div> </div>
<div className="flex items-center justify-end space-x-2"> <div className="flex items-center justify-end space-x-2">
<Button <Button
onClick={() => onClick={() =>
handleQAStatusChange(qa.id, "approved") handleQAStatusChange(qa.id, "approved")
} }
size="sm" size="sm"
variant={ variant={
qa.status === "approved" ? "default" : "outline" qa.status === "approved" ? "default" : "outline"
} }
className={ className={
qa.status === "approved" qa.status === "approved"
? "bg-green-600 hover:bg-green-700" ? "bg-green-600 hover:bg-green-700"
: "" : ""
} }
> >
<ThumbsUp className="w-4 h-4 mr-1" /> <ThumbsUp className="w-4 h-4 mr-1" />
</Button> </Button>
<Button <Button
onClick={() => onClick={() =>
handleQAStatusChange(qa.id, "rejected") handleQAStatusChange(qa.id, "rejected")
} }
size="sm" size="sm"
variant={ variant={
qa.status === "rejected" qa.status === "rejected"
? "destructive" ? "destructive"
: "outline" : "outline"
} }
> >
<ThumbsDown className="w-4 h-4 mr-1" /> <ThumbsDown className="w-4 h-4 mr-1" />
</Button> </Button>
</div> </div>
</div> </div>
</Card> </Card>
))} ))}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
) : ( ) : (
<div className="flex-1 flex items-center justify-center"> <div className="flex-1 flex items-center justify-center">
<div className="text-center"> <div className="text-center">
<File className="w-12 h-12 text-gray-400 mx-auto mb-4" /> <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 className="text-lg font-medium text-gray-900 mb-2">
</h3> </h3>
<p className="text-gray-500"> <p className="text-gray-500">
</p> </p>
</div> </div>
</div> </div>
)} )}
</div> </div>
</div> </div>
); );
} }

View File

@@ -1,346 +1,346 @@
import type React from "react"; import type React from "react";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Card, Button, Input, Select, Divider, Form, message } from "antd"; import { Card, Button, Input, Select, Divider, Form, message } from "antd";
import TextArea from "antd/es/input/TextArea"; import TextArea from "antd/es/input/TextArea";
import { import {
DatabaseOutlined, DatabaseOutlined,
CheckOutlined, CheckOutlined,
PlusOutlined, PlusOutlined,
} from "@ant-design/icons"; } from "@ant-design/icons";
import { mockTemplates } from "@/mock/annotation"; import { mockTemplates } from "@/mock/annotation";
import CustomTemplateDialog from "./components/CustomTemplateDialog"; import CustomTemplateDialog from "./components/CustomTemplateDialog";
import { Link, useNavigate } from "react-router"; import { Link, useNavigate } from "react-router";
import { ArrowLeft } from "lucide-react"; import { ArrowLeft } from "lucide-react";
import { queryDatasetsUsingGet } from "../../DataManagement/dataset.api"; import { queryDatasetsUsingGet } from "../../DataManagement/dataset.api";
import { import {
DatasetType, DatasetType,
type Dataset, type Dataset,
} from "@/pages/DataManagement/dataset.model"; } from "@/pages/DataManagement/dataset.model";
interface Template { interface Template {
id: string; id: string;
name: string; name: string;
category: string; category: string;
description: string; description: string;
type: "text" | "image"; type: "text" | "image";
preview?: string; preview?: string;
icon: React.ReactNode; icon: React.ReactNode;
isCustom?: boolean; isCustom?: boolean;
} }
const templateCategories = ["Computer Vision", "Natural Language Processing"]; const templateCategories = ["Computer Vision", "Natural Language Processing"];
export default function AnnotationTaskCreate() { export default function AnnotationTaskCreate() {
const navigate = useNavigate(); const navigate = useNavigate();
const [form] = Form.useForm(); const [form] = Form.useForm();
const [showCustomTemplateDialog, setShowCustomTemplateDialog] = const [showCustomTemplateDialog, setShowCustomTemplateDialog] =
useState(false); useState(false);
const [selectedCategory, setSelectedCategory] = useState("Computer Vision"); const [selectedCategory, setSelectedCategory] = useState("Computer Vision");
const [searchQuery, setSearchQuery] = useState(""); const [searchQuery, setSearchQuery] = useState("");
const [datasetFilter, setDatasetFilter] = useState("all"); const [datasetFilter, setDatasetFilter] = useState("all");
const [selectedTemplate, setSelectedTemplate] = useState<Template | null>( const [selectedTemplate, setSelectedTemplate] = useState<Template | null>(
null null
); );
const [datasets, setDatasets] = useState<Dataset[]>([]); const [datasets, setDatasets] = useState<Dataset[]>([]);
const [selectedDataset, setSelectedDataset] = useState<Dataset | null>(null); const [selectedDataset, setSelectedDataset] = useState<Dataset | null>(null);
// 用于Form的受控数据 // 用于Form的受控数据
const [formValues, setFormValues] = useState({ const [formValues, setFormValues] = useState({
name: "", name: "",
description: "", description: "",
datasetId: "", datasetId: "",
templateId: "", templateId: "",
}); });
const fetchDatasets = async () => { const fetchDatasets = async () => {
const { data } = await queryDatasetsUsingGet(); const { data } = await queryDatasetsUsingGet();
setDatasets(data.results || []); setDatasets(data.results || []);
}; };
useEffect(() => { useEffect(() => {
fetchDatasets(); fetchDatasets();
}, []); }, []);
const filteredTemplates = mockTemplates.filter( const filteredTemplates = mockTemplates.filter(
(template) => template.category === selectedCategory (template) => template.category === selectedCategory
); );
const handleDatasetSelect = (datasetId: string) => { const handleDatasetSelect = (datasetId: string) => {
const dataset = datasets.find((ds) => ds.id === datasetId) || null; const dataset = datasets.find((ds) => ds.id === datasetId) || null;
setSelectedDataset(dataset); setSelectedDataset(dataset);
setFormValues((prev) => ({ ...prev, datasetId })); setFormValues((prev) => ({ ...prev, datasetId }));
if (dataset?.type === DatasetType.PRETRAIN_IMAGE) { if (dataset?.type === DatasetType.PRETRAIN_IMAGE) {
setSelectedCategory("Computer Vision"); setSelectedCategory("Computer Vision");
} else if (dataset?.type === DatasetType.PRETRAIN_TEXT) { } else if (dataset?.type === DatasetType.PRETRAIN_TEXT) {
setSelectedCategory("Natural Language Processing"); setSelectedCategory("Natural Language Processing");
} }
setSelectedTemplate(null); setSelectedTemplate(null);
setFormValues((prev) => ({ ...prev, templateId: "" })); setFormValues((prev) => ({ ...prev, templateId: "" }));
}; };
const handleTemplateSelect = (template: Template) => { const handleTemplateSelect = (template: Template) => {
setSelectedTemplate(template); setSelectedTemplate(template);
setFormValues((prev) => ({ ...prev, templateId: template.id })); setFormValues((prev) => ({ ...prev, templateId: template.id }));
}; };
const handleValuesChange = (_, allValues) => { const handleValuesChange = (_, allValues) => {
setFormValues({ ...formValues, ...allValues }); setFormValues({ ...formValues, ...allValues });
}; };
const handleSubmit = async () => { const handleSubmit = async () => {
try { try {
const values = await form.validateFields(); const values = await form.validateFields();
const dataset = datasets.find((ds) => ds.id === values.datasetId); const dataset = datasets.find((ds) => ds.id === values.datasetId);
const template = mockTemplates.find( const template = mockTemplates.find(
(tpl) => tpl.id === values.templateId (tpl) => tpl.id === values.templateId
); );
if (!dataset) { if (!dataset) {
message.error("请选择数据集"); message.error("请选择数据集");
return; return;
} }
if (!template) { if (!template) {
message.error("请选择标注模板"); message.error("请选择标注模板");
return; return;
} }
const taskData = { const taskData = {
name: values.name, name: values.name,
description: values.description, description: values.description,
dataset, dataset,
template, template,
}; };
// onCreateTask(taskData); // 实际创建逻辑 // onCreateTask(taskData); // 实际创建逻辑
message.success("标注任务创建成功"); message.success("标注任务创建成功");
navigate("/data/annotation"); navigate("/data/annotation");
} catch (e) { } catch (e) {
// 校验失败 // 校验失败
} }
}; };
const handleSaveCustomTemplate = (templateData: any) => { const handleSaveCustomTemplate = (templateData: any) => {
setSelectedTemplate(templateData); setSelectedTemplate(templateData);
setFormValues((prev) => ({ ...prev, templateId: templateData.id })); setFormValues((prev) => ({ ...prev, templateId: templateData.id }));
message.success(`自定义模板 "${templateData.name}" 已创建`); message.success(`自定义模板 "${templateData.name}" 已创建`);
}; };
return ( return (
<div className="flex-overflow-auto"> <div className="flex-overflow-auto">
{/* Header */} {/* Header */}
<div className="flex items-center mb-2"> <div className="flex items-center mb-2">
<Link to="/data/annotation"> <Link to="/data/annotation">
<Button type="text"> <Button type="text">
<ArrowLeft className="w-4 h-4 mr-1" /> <ArrowLeft className="w-4 h-4 mr-1" />
</Button> </Button>
</Link> </Link>
<h1 className="text-xl font-bold bg-clip-text"></h1> <h1 className="text-xl font-bold bg-clip-text"></h1>
</div> </div>
<div className="flex-overflow-auto bg-white rounded-lg shadow-sm"> <div className="flex-overflow-auto bg-white rounded-lg shadow-sm">
<div className="flex-1 overflow-y-auto p-6"> <div className="flex-1 overflow-y-auto p-6">
<Form <Form
form={form} form={form}
initialValues={formValues} initialValues={formValues}
onValuesChange={handleValuesChange} onValuesChange={handleValuesChange}
layout="vertical" layout="vertical"
> >
{/* 基本信息 */} {/* 基本信息 */}
<h2 className="font-medium text-gray-900 text-lg mb-2"></h2> <h2 className="font-medium text-gray-900 text-lg mb-2"></h2>
<Form.Item <Form.Item
label="任务名称" label="任务名称"
name="name" name="name"
rules={[{ required: true, message: "请输入任务名称" }]} rules={[{ required: true, message: "请输入任务名称" }]}
> >
<Input placeholder="输入任务名称" /> <Input placeholder="输入任务名称" />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
label="任务描述" label="任务描述"
name="description" name="description"
rules={[{ required: true, message: "请输入任务描述" }]} rules={[{ required: true, message: "请输入任务描述" }]}
> >
<TextArea placeholder="详细描述标注任务的要求和目标" rows={3} /> <TextArea placeholder="详细描述标注任务的要求和目标" rows={3} />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
label="选择数据集" label="选择数据集"
name="datasetId" name="datasetId"
rules={[{ required: true, message: "请选择数据集" }]} rules={[{ required: true, message: "请选择数据集" }]}
> >
<Select <Select
optionFilterProp="children" optionFilterProp="children"
value={formValues.datasetId} value={formValues.datasetId}
onChange={handleDatasetSelect} onChange={handleDatasetSelect}
placeholder="请选择数据集" placeholder="请选择数据集"
size="large" size="large"
options={datasets.map((dataset) => ({ options={datasets.map((dataset) => ({
label: ( label: (
<div className="flex items-center justify-between gap-3 py-2"> <div className="flex items-center justify-between gap-3 py-2">
<div className="font-medium text-gray-900"> <div className="font-medium text-gray-900">
{dataset?.icon || <DatabaseOutlined className="mr-2" />} {dataset?.icon || <DatabaseOutlined className="mr-2" />}
{dataset.name} {dataset.name}
</div> </div>
<div className="text-xs text-gray-500"> <div className="text-xs text-gray-500">
{dataset?.fileCount} {dataset.size} {dataset?.fileCount} {dataset.size}
</div> </div>
</div> </div>
), ),
value: dataset.id, value: dataset.id,
}))} }))}
/> />
</Form.Item> </Form.Item>
{/* 模板选择 */} {/* 模板选择 */}
<h2 className="font-medium text-gray-900 text-lg mt-6 mb-2 flex items-center gap-2"> <h2 className="font-medium text-gray-900 text-lg mt-6 mb-2 flex items-center gap-2">
</h2> </h2>
<Form.Item <Form.Item
name="templateId" name="templateId"
rules={[{ required: true, message: "请选择标注模板" }]} rules={[{ required: true, message: "请选择标注模板" }]}
> >
<div className="flex"> <div className="flex">
{/* Category Sidebar */} {/* Category Sidebar */}
<div className="w-64 pr-6 border-r border-gray-200"> <div className="w-64 pr-6 border-r border-gray-200">
<div className="space-y-2"> <div className="space-y-2">
{templateCategories.map((category) => { {templateCategories.map((category) => {
const isAvailable = const isAvailable =
selectedDataset?.type === "image" selectedDataset?.type === "image"
? category === "Computer Vision" ? category === "Computer Vision"
: category === "Natural Language Processing"; : category === "Natural Language Processing";
return ( return (
<Button <Button
key={category} key={category}
type={ type={
selectedCategory === category && isAvailable selectedCategory === category && isAvailable
? "primary" ? "primary"
: "default" : "default"
} }
block block
disabled={!isAvailable} disabled={!isAvailable}
onClick={() => onClick={() =>
isAvailable && setSelectedCategory(category) isAvailable && setSelectedCategory(category)
} }
style={{ textAlign: "left", marginBottom: 8 }} style={{ textAlign: "left", marginBottom: 8 }}
> >
{category} {category}
</Button> </Button>
); );
})} })}
<Button <Button
type="dashed" type="dashed"
block block
icon={<PlusOutlined />} icon={<PlusOutlined />}
onClick={() => setShowCustomTemplateDialog(true)} onClick={() => setShowCustomTemplateDialog(true)}
> >
</Button> </Button>
</div> </div>
</div> </div>
{/* Template Grid */} {/* Template Grid */}
<div className="flex-1 pl-6"> <div className="flex-1 pl-6">
<div className="max-h-96 overflow-auto"> <div className="max-h-96 overflow-auto">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{filteredTemplates.map((template) => ( {filteredTemplates.map((template) => (
<div <div
key={template.id} key={template.id}
className={`border rounded-lg cursor-pointer transition-all hover:shadow-md ${ className={`border rounded-lg cursor-pointer transition-all hover:shadow-md ${
formValues.templateId === template.id formValues.templateId === template.id
? "border-blue-500 bg-blue-50" ? "border-blue-500 bg-blue-50"
: "border-gray-200" : "border-gray-200"
}`} }`}
onClick={() => handleTemplateSelect(template)} onClick={() => handleTemplateSelect(template)}
> >
{template.preview && ( {template.preview && (
<div className="aspect-video bg-gray-100 rounded-t-lg overflow-hidden"> <div className="aspect-video bg-gray-100 rounded-t-lg overflow-hidden">
<img <img
src={template.preview || "/placeholder.svg"} src={template.preview || "/placeholder.svg"}
alt={template.name} alt={template.name}
className="w-full h-full object-cover" className="w-full h-full object-cover"
/> />
</div> </div>
)} )}
<div className="p-3"> <div className="p-3">
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between mb-2">
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
{template.icon} {template.icon}
<span className="font-medium text-sm"> <span className="font-medium text-sm">
{template.name} {template.name}
</span> </span>
</div> </div>
</div> </div>
<p className="text-xs text-gray-600"> <p className="text-xs text-gray-600">
{template.description} {template.description}
</p> </p>
</div> </div>
</div> </div>
))} ))}
{/* Custom Template Option */} {/* Custom Template Option */}
<div <div
className={`border-2 border-dashed rounded-lg cursor-pointer transition-all hover:border-gray-400 ${ className={`border-2 border-dashed rounded-lg cursor-pointer transition-all hover:border-gray-400 ${
selectedTemplate?.isCustom selectedTemplate?.isCustom
? "border-blue-500 bg-blue-50" ? "border-blue-500 bg-blue-50"
: "border-gray-300" : "border-gray-300"
}`} }`}
onClick={() => setShowCustomTemplateDialog(true)} onClick={() => setShowCustomTemplateDialog(true)}
> >
<div className="aspect-video bg-gray-50 rounded-t-lg flex items-center justify-center"> <div className="aspect-video bg-gray-50 rounded-t-lg flex items-center justify-center">
<PlusOutlined <PlusOutlined
style={{ fontSize: 32, color: "#bbb" }} style={{ fontSize: 32, color: "#bbb" }}
/> />
</div> </div>
<div className="p-3"> <div className="p-3">
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between mb-2">
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<PlusOutlined /> <PlusOutlined />
<span className="font-medium text-sm"> <span className="font-medium text-sm">
</span> </span>
</div> </div>
{selectedTemplate?.isCustom && ( {selectedTemplate?.isCustom && (
<CheckOutlined style={{ color: "#1677ff" }} /> <CheckOutlined style={{ color: "#1677ff" }} />
)} )}
</div> </div>
<p className="text-xs text-gray-600"> <p className="text-xs text-gray-600">
</p> </p>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{selectedTemplate && ( {selectedTemplate && (
<div className="mt-4 p-3 bg-blue-50 rounded-lg"> <div className="mt-4 p-3 bg-blue-50 rounded-lg">
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<span <span
className="text-sm font-medium" className="text-sm font-medium"
style={{ color: "#1677ff" }} style={{ color: "#1677ff" }}
> >
</span> </span>
</div> </div>
<p <p
className="text-sm" className="text-sm"
style={{ color: "#1677ff", marginTop: 4 }} style={{ color: "#1677ff", marginTop: 4 }}
> >
{selectedTemplate.name} - {selectedTemplate.description} {selectedTemplate.name} - {selectedTemplate.description}
</p> </p>
</div> </div>
)} )}
</Form.Item> </Form.Item>
</Form> </Form>
</div> </div>
<div className="flex gap-2 justify-end border-t border-gray-200 p-6"> <div className="flex gap-2 justify-end border-t border-gray-200 p-6">
<Button onClick={() => navigate("/data/annotation")}></Button> <Button onClick={() => navigate("/data/annotation")}></Button>
<Button type="primary" onClick={handleSubmit}> <Button type="primary" onClick={handleSubmit}>
</Button> </Button>
</div> </div>
</div> </div>
{/* Custom Template Dialog */} {/* Custom Template Dialog */}
<CustomTemplateDialog <CustomTemplateDialog
open={showCustomTemplateDialog} open={showCustomTemplateDialog}
onOpenChange={setShowCustomTemplateDialog} onOpenChange={setShowCustomTemplateDialog}
onSaveTemplate={handleSaveCustomTemplate} onSaveTemplate={handleSaveCustomTemplate}
datasetType={selectedDataset?.type || "image"} datasetType={selectedDataset?.type || "image"}
/> />
</div> </div>
); );
} }

View File

@@ -1,192 +1,192 @@
import { queryDatasetsUsingGet } from "@/pages/DataManagement/dataset.api"; import { queryDatasetsUsingGet } from "@/pages/DataManagement/dataset.api";
import { mapDataset } from "@/pages/DataManagement/dataset.const"; import { mapDataset } from "@/pages/DataManagement/dataset.const";
import { Button, Form, Input, Modal, Select, message } from "antd"; import { Button, Form, Input, Modal, Select, message } from "antd";
import TextArea from "antd/es/input/TextArea"; import TextArea from "antd/es/input/TextArea";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { createAnnotationTaskUsingPost, queryAnnotationTemplatesUsingGet } from "../../annotation.api"; import { createAnnotationTaskUsingPost, queryAnnotationTemplatesUsingGet } from "../../annotation.api";
import { Dataset } from "@/pages/DataManagement/dataset.model"; import { Dataset } from "@/pages/DataManagement/dataset.model";
import type { AnnotationTemplate } from "../../annotation.model"; import type { AnnotationTemplate } from "../../annotation.model";
export default function CreateAnnotationTask({ export default function CreateAnnotationTask({
open, open,
onClose, onClose,
onRefresh, onRefresh,
}: { }: {
open: boolean; open: boolean;
onClose: () => void; onClose: () => void;
onRefresh: () => void; onRefresh: () => void;
}) { }) {
const [form] = Form.useForm(); const [form] = Form.useForm();
const [datasets, setDatasets] = useState<Dataset[]>([]); const [datasets, setDatasets] = useState<Dataset[]>([]);
const [templates, setTemplates] = useState<AnnotationTemplate[]>([]); const [templates, setTemplates] = useState<AnnotationTemplate[]>([]);
const [submitting, setSubmitting] = useState(false); const [submitting, setSubmitting] = useState(false);
const [nameManuallyEdited, setNameManuallyEdited] = useState(false); const [nameManuallyEdited, setNameManuallyEdited] = useState(false);
useEffect(() => { useEffect(() => {
if (!open) return; if (!open) return;
const fetchData = async () => { const fetchData = async () => {
try { try {
// Fetch datasets // Fetch datasets
const { data: datasetData } = await queryDatasetsUsingGet({ const { data: datasetData } = await queryDatasetsUsingGet({
page: 0, page: 0,
pageSize: 1000, // Use camelCase for HTTP params pageSize: 1000, // Use camelCase for HTTP params
}); });
setDatasets(datasetData.content.map(mapDataset) || []); setDatasets(datasetData.content.map(mapDataset) || []);
// Fetch templates // Fetch templates
const templateResponse = await queryAnnotationTemplatesUsingGet({ const templateResponse = await queryAnnotationTemplatesUsingGet({
page: 1, page: 1,
size: 100, // Backend max is 100 (template API uses 'size' not 'pageSize') size: 100, // Backend max is 100 (template API uses 'size' not 'pageSize')
}); });
// The API returns: {code, message, data: {content, total, page, ...}} // The API returns: {code, message, data: {content, total, page, ...}}
if (templateResponse.code === 200 && templateResponse.data) { if (templateResponse.code === 200 && templateResponse.data) {
const fetchedTemplates = templateResponse.data.content || []; const fetchedTemplates = templateResponse.data.content || [];
console.log("Fetched templates:", fetchedTemplates); console.log("Fetched templates:", fetchedTemplates);
setTemplates(fetchedTemplates); setTemplates(fetchedTemplates);
} else { } else {
console.error("Failed to fetch templates:", templateResponse); console.error("Failed to fetch templates:", templateResponse);
setTemplates([]); setTemplates([]);
} }
} catch (error) { } catch (error) {
console.error("Error fetching data:", error); console.error("Error fetching data:", error);
setTemplates([]); setTemplates([]);
} }
}; };
fetchData(); fetchData();
}, [open]); }, [open]);
// Reset form and manual-edit flag when modal opens // Reset form and manual-edit flag when modal opens
useEffect(() => { useEffect(() => {
if (open) { if (open) {
form.resetFields(); form.resetFields();
setNameManuallyEdited(false); setNameManuallyEdited(false);
} }
}, [open, form]); }, [open, form]);
const handleSubmit = async () => { const handleSubmit = async () => {
try { try {
const values = await form.validateFields(); const values = await form.validateFields();
setSubmitting(true); setSubmitting(true);
// Send templateId instead of labelingConfig // Send templateId instead of labelingConfig
const requestData = { const requestData = {
name: values.name, name: values.name,
description: values.description, description: values.description,
datasetId: values.datasetId, datasetId: values.datasetId,
templateId: values.templateId, templateId: values.templateId,
}; };
await createAnnotationTaskUsingPost(requestData); await createAnnotationTaskUsingPost(requestData);
message?.success?.("创建标注任务成功"); message?.success?.("创建标注任务成功");
onClose(); onClose();
onRefresh(); onRefresh();
} catch (err: any) { } catch (err: any) {
console.error("Create annotation task failed", err); console.error("Create annotation task failed", err);
const msg = err?.message || err?.data?.message || "创建失败,请稍后重试"; const msg = err?.message || err?.data?.message || "创建失败,请稍后重试";
(message as any)?.error?.(msg); (message as any)?.error?.(msg);
} finally { } finally {
setSubmitting(false); setSubmitting(false);
} }
}; };
return ( return (
<Modal <Modal
open={open} open={open}
onCancel={onClose} onCancel={onClose}
title="创建标注任务" title="创建标注任务"
footer={ footer={
<> <>
<Button onClick={onClose} disabled={submitting}> <Button onClick={onClose} disabled={submitting}>
</Button> </Button>
<Button type="primary" onClick={handleSubmit} loading={submitting}> <Button type="primary" onClick={handleSubmit} loading={submitting}>
</Button> </Button>
</> </>
} }
width={800} width={800}
> >
<Form form={form} layout="vertical"> <Form form={form} layout="vertical">
{/* 数据集 与 标注工程名称 并排显示(数据集在左) */} {/* 数据集 与 标注工程名称 并排显示(数据集在左) */}
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<Form.Item <Form.Item
label="数据集" label="数据集"
name="datasetId" name="datasetId"
rules={[{ required: true, message: "请选择数据集" }]} rules={[{ required: true, message: "请选择数据集" }]}
> >
<Select <Select
placeholder="请选择数据集" placeholder="请选择数据集"
options={datasets.map((dataset) => { options={datasets.map((dataset) => {
return { return {
label: ( label: (
<div className="flex items-center justify-between gap-3 py-2"> <div className="flex items-center justify-between gap-3 py-2">
<div className="flex items-center font-sm text-gray-900"> <div className="flex items-center font-sm text-gray-900">
<span className="mr-2">{(dataset as any).icon}</span> <span className="mr-2">{(dataset as any).icon}</span>
<span>{dataset.name}</span> <span>{dataset.name}</span>
</div> </div>
<div className="text-xs text-gray-500">{dataset.size}</div> <div className="text-xs text-gray-500">{dataset.size}</div>
</div> </div>
), ),
value: dataset.id, value: dataset.id,
}; };
})} })}
onChange={(value) => { onChange={(value) => {
// 如果用户未手动修改名称,则用数据集名称作为默认任务名 // 如果用户未手动修改名称,则用数据集名称作为默认任务名
if (!nameManuallyEdited) { if (!nameManuallyEdited) {
const ds = datasets.find((d) => d.id === value); const ds = datasets.find((d) => d.id === value);
if (ds) { if (ds) {
form.setFieldsValue({ name: ds.name }); form.setFieldsValue({ name: ds.name });
} }
} }
}} }}
/> />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
label="标注工程名称" label="标注工程名称"
name="name" name="name"
rules={[{ required: true, message: "请输入任务名称" }]} rules={[{ required: true, message: "请输入任务名称" }]}
> >
<Input <Input
placeholder="输入标注工程名称" placeholder="输入标注工程名称"
onChange={() => setNameManuallyEdited(true)} onChange={() => setNameManuallyEdited(true)}
/> />
</Form.Item> </Form.Item>
</div> </div>
{/* 描述变为可选 */} {/* 描述变为可选 */}
<Form.Item label="描述" name="description"> <Form.Item label="描述" name="description">
<TextArea placeholder="(可选)详细描述标注任务的要求和目标" rows={3} /> <TextArea placeholder="(可选)详细描述标注任务的要求和目标" rows={3} />
</Form.Item> </Form.Item>
{/* 标注模板选择 */} {/* 标注模板选择 */}
<Form.Item <Form.Item
label="标注模板" label="标注模板"
name="templateId" name="templateId"
rules={[{ required: true, message: "请选择标注模板" }]} rules={[{ required: true, message: "请选择标注模板" }]}
> >
<Select <Select
placeholder={templates.length === 0 ? "暂无可用模板,请先创建模板" : "请选择标注模板"} placeholder={templates.length === 0 ? "暂无可用模板,请先创建模板" : "请选择标注模板"}
showSearch showSearch
optionFilterProp="label" optionFilterProp="label"
notFoundContent={templates.length === 0 ? "暂无模板,请前往「标注模板」页面创建" : "未找到匹配的模板"} notFoundContent={templates.length === 0 ? "暂无模板,请前往「标注模板」页面创建" : "未找到匹配的模板"}
options={templates.map((template) => ({ options={templates.map((template) => ({
label: template.name, label: template.name,
value: template.id, value: template.id,
// Add description as subtitle // Add description as subtitle
title: template.description, title: template.description,
}))} }))}
optionRender={(option) => ( optionRender={(option) => (
<div> <div>
<div style={{ fontWeight: 500 }}>{option.label}</div> <div style={{ fontWeight: 500 }}>{option.label}</div>
{option.data.title && ( {option.data.title && (
<div style={{ fontSize: 12, color: '#999', marginTop: 2 }}> <div style={{ fontSize: 12, color: '#999', marginTop: 2 }}>
{option.data.title} {option.data.title}
</div> </div>
)} )}
</div> </div>
)} )}
/> />
</Form.Item> </Form.Item>
</Form> </Form>
</Modal> </Modal>
); );
} }

View File

@@ -1,195 +1,195 @@
import { queryDatasetsUsingGet } from "@/pages/DataManagement/dataset.api"; import { queryDatasetsUsingGet } from "@/pages/DataManagement/dataset.api";
import { mapDataset } from "@/pages/DataManagement/dataset.const"; import { mapDataset } from "@/pages/DataManagement/dataset.const";
import { Button, Form, Input, Modal, Select, message } from "antd"; import { Button, Form, Input, Modal, Select, message } from "antd";
import TextArea from "antd/es/input/TextArea"; import TextArea from "antd/es/input/TextArea";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { createAnnotationTaskUsingPost, queryAnnotationTemplatesUsingGet } from "../../annotation.api"; import { createAnnotationTaskUsingPost, queryAnnotationTemplatesUsingGet } from "../../annotation.api";
import { Dataset } from "@/pages/DataManagement/dataset.model"; import { Dataset } from "@/pages/DataManagement/dataset.model";
import type { AnnotationTemplate } from "../../annotation.model"; import type { AnnotationTemplate } from "../../annotation.model";
export default function CreateAnnotationTask({ export default function CreateAnnotationTask({
open, open,
onClose, onClose,
onRefresh, onRefresh,
}: { }: {
open: boolean; open: boolean;
onClose: () => void; onClose: () => void;
onRefresh: () => void; onRefresh: () => void;
}) { }) {
const [form] = Form.useForm(); const [form] = Form.useForm();
const [datasets, setDatasets] = useState<Dataset[]>([]); const [datasets, setDatasets] = useState<Dataset[]>([]);
const [templates, setTemplates] = useState<AnnotationTemplate[]>([]); const [templates, setTemplates] = useState<AnnotationTemplate[]>([]);
const [submitting, setSubmitting] = useState(false); const [submitting, setSubmitting] = useState(false);
const [nameManuallyEdited, setNameManuallyEdited] = useState(false); const [nameManuallyEdited, setNameManuallyEdited] = useState(false);
useEffect(() => { useEffect(() => {
if (!open) return; if (!open) return;
const fetchData = async () => { const fetchData = async () => {
try { try {
// Fetch datasets // Fetch datasets
const { data: datasetData } = await queryDatasetsUsingGet({ const { data: datasetData } = await queryDatasetsUsingGet({
page: 0, page: 0,
pageSize: 1000, // Use camelCase for HTTP params pageSize: 1000, // Use camelCase for HTTP params
}); });
setDatasets(datasetData.content.map(mapDataset) || []); setDatasets(datasetData.content.map(mapDataset) || []);
// Fetch templates // Fetch templates
const templateResponse = await queryAnnotationTemplatesUsingGet({ const templateResponse = await queryAnnotationTemplatesUsingGet({
page: 1, page: 1,
size: 100, // Backend max is 100 (template API uses 'size' not 'pageSize') size: 100, // Backend max is 100 (template API uses 'size' not 'pageSize')
}); });
// The API returns: {code, message, data: {content, total, page, ...}} // The API returns: {code, message, data: {content, total, page, ...}}
if (templateResponse.code === 200 && templateResponse.data) { if (templateResponse.code === 200 && templateResponse.data) {
const fetchedTemplates = templateResponse.data.content || []; const fetchedTemplates = templateResponse.data.content || [];
console.log("Fetched templates:", fetchedTemplates); console.log("Fetched templates:", fetchedTemplates);
setTemplates(fetchedTemplates); setTemplates(fetchedTemplates);
} else { } else {
console.error("Failed to fetch templates:", templateResponse); console.error("Failed to fetch templates:", templateResponse);
setTemplates([]); setTemplates([]);
} }
} catch (error) { } catch (error) {
console.error("Error fetching data:", error); console.error("Error fetching data:", error);
setTemplates([]); setTemplates([]);
} }
}; };
fetchData(); fetchData();
}, [open]); }, [open]);
// Reset form and manual-edit flag when modal opens // Reset form and manual-edit flag when modal opens
useEffect(() => { useEffect(() => {
if (open) { if (open) {
form.resetFields(); form.resetFields();
setNameManuallyEdited(false); setNameManuallyEdited(false);
} }
}, [open, form]); }, [open, form]);
const handleSubmit = async () => { const handleSubmit = async () => {
try { try {
const values = await form.validateFields(); const values = await form.validateFields();
setSubmitting(true); setSubmitting(true);
// Send templateId instead of labelingConfig // Send templateId instead of labelingConfig
const requestData = { const requestData = {
name: values.name, name: values.name,
description: values.description, description: values.description,
datasetId: values.datasetId, datasetId: values.datasetId,
templateId: values.templateId, templateId: values.templateId,
}; };
await createAnnotationTaskUsingPost(requestData); await createAnnotationTaskUsingPost(requestData);
message?.success?.("创建标注任务成功"); message?.success?.("创建标注任务成功");
onClose(); onClose();
onRefresh(); onRefresh();
} catch (err: any) { } catch (err: any) {
console.error("Create annotation task failed", err); console.error("Create annotation task failed", err);
const msg = err?.message || err?.data?.message || "创建失败,请稍后重试"; const msg = err?.message || err?.data?.message || "创建失败,请稍后重试";
(message as any)?.error?.(msg); (message as any)?.error?.(msg);
} finally { } finally {
setSubmitting(false); setSubmitting(false);
} }
}; };
return ( return (
<Modal <Modal
open={open} open={open}
onCancel={onClose} onCancel={onClose}
title="创建标注任务" title="创建标注任务"
footer={ footer={
<> <>
<Button onClick={onClose} disabled={submitting}> <Button onClick={onClose} disabled={submitting}>
</Button> </Button>
<Button type="primary" onClick={handleSubmit} loading={submitting}> <Button type="primary" onClick={handleSubmit} loading={submitting}>
</Button> </Button>
</> </>
} }
width={800} width={800}
> >
<Form form={form} layout="vertical"> <Form form={form} layout="vertical">
{/* 数据集 与 标注工程名称 并排显示(数据集在左) */} {/* 数据集 与 标注工程名称 并排显示(数据集在左) */}
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<Form.Item <Form.Item
label="数据集" label="数据集"
name="datasetId" name="datasetId"
rules={[{ required: true, message: "请选择数据集" }]} rules={[{ required: true, message: "请选择数据集" }]}
> >
<Select <Select
placeholder="请选择数据集" placeholder="请选择数据集"
options={datasets.map((dataset) => { options={datasets.map((dataset) => {
return { return {
label: ( label: (
<div className="flex items-center justify-between gap-3 py-2"> <div className="flex items-center justify-between gap-3 py-2">
<div className="flex items-center font-sm text-gray-900"> <div className="flex items-center font-sm text-gray-900">
<span className="mr-2">{(dataset as any).icon}</span> <span className="mr-2">{(dataset as any).icon}</span>
<span>{dataset.name}</span> <span>{dataset.name}</span>
</div> </div>
<div className="text-xs text-gray-500">{dataset.size}</div> <div className="text-xs text-gray-500">{dataset.size}</div>
</div> </div>
), ),
value: dataset.id, value: dataset.id,
}; };
})} })}
onChange={(value) => { onChange={(value) => {
// 如果用户未手动修改名称,则用数据集名称作为默认任务名 // 如果用户未手动修改名称,则用数据集名称作为默认任务名
if (!nameManuallyEdited) { if (!nameManuallyEdited) {
const ds = datasets.find((d) => d.id === value); const ds = datasets.find((d) => d.id === value);
if (ds) { if (ds) {
form.setFieldsValue({ name: ds.name }); form.setFieldsValue({ name: ds.name });
} }
} }
}} }}
/> />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
label="标注工程名称" label="标注工程名称"
name="name" name="name"
rules={[{ required: true, message: "请输入任务名称" }]} rules={[{ required: true, message: "请输入任务名称" }]}
> >
<Input <Input
placeholder="输入标注工程名称" placeholder="输入标注工程名称"
onChange={() => setNameManuallyEdited(true)} onChange={() => setNameManuallyEdited(true)}
/> />
</Form.Item> </Form.Item>
</div> </div>
{/* 描述变为可选 */} {/* 描述变为可选 */}
<Form.Item label="描述" name="description"> <Form.Item label="描述" name="description">
<TextArea placeholder="(可选)详细描述标注任务的要求和目标" rows={3} /> <TextArea placeholder="(可选)详细描述标注任务的要求和目标" rows={3} />
</Form.Item> </Form.Item>
{/* 标注模板选择 */} {/* 标注模板选择 */}
<Form.Item <Form.Item
label="标注模板" label="标注模板"
name="templateId" name="templateId"
rules={[{ required: true, message: "请选择标注模板" }]} rules={[{ required: true, message: "请选择标注模板" }]}
> >
<Select <Select
placeholder={templates.length === 0 ? "暂无可用模板,请先创建模板" : "请选择标注模板"} placeholder={templates.length === 0 ? "暂无可用模板,请先创建模板" : "请选择标注模板"}
showSearch showSearch
optionFilterProp="label" optionFilterProp="label"
notFoundContent={templates.length === 0 ? "暂无模板,请前往「标注模板」页面创建" : "未找到匹配的模板"} notFoundContent={templates.length === 0 ? "暂无模板,请前往「标注模板」页面创建" : "未找到匹配的模板"}
options={templates.map((template) => ({ options={templates.map((template) => ({
label: template.name, label: template.name,
value: template.id, value: template.id,
// Add description as subtitle // Add description as subtitle
title: template.description, title: template.description,
}))} }))}
optionRender={(option) => ( optionRender={(option) => (
<div> <div>
<div style={{ fontWeight: 500 }}>{option.label}</div> <div style={{ fontWeight: 500 }}>{option.label}</div>
{option.data.title && ( {option.data.title && (
<div style={{ fontSize: 12, color: '#999', marginTop: 2 }}> <div style={{ fontSize: 12, color: '#999', marginTop: 2 }}>
{option.data.title} {option.data.title}
</div> </div>
)} )}
</div> </div>
)} )}
/> />
</Form.Item> </Form.Item>
</Form> </Form>
</Modal> </Modal>
); );
} }

View File

@@ -1,225 +1,225 @@
import { useState } from "react"; import { useState } from "react";
import { import {
Modal, Modal,
Input, Input,
Card, Card,
message, message,
Divider, Divider,
Radio, Radio,
Form, Form,
} from "antd"; } from "antd";
import { import {
AppstoreOutlined, AppstoreOutlined,
BorderOutlined, BorderOutlined,
DotChartOutlined, DotChartOutlined,
EditOutlined, EditOutlined,
CheckSquareOutlined, CheckSquareOutlined,
BarsOutlined, BarsOutlined,
DeploymentUnitOutlined, DeploymentUnitOutlined,
} from "@ant-design/icons"; } from "@ant-design/icons";
interface CustomTemplateDialogProps { interface CustomTemplateDialogProps {
open: boolean; open: boolean;
onOpenChange: (open: boolean) => void; onOpenChange: (open: boolean) => void;
onSaveTemplate: (templateData: any) => void; onSaveTemplate: (templateData: any) => void;
datasetType: "text" | "image"; datasetType: "text" | "image";
} }
const { TextArea } = Input; const { TextArea } = Input;
const defaultImageTemplate = `<View style="display: flex; flex-direction: column; height: 100vh; overflow: auto;"> const defaultImageTemplate = `<View style="display: flex; flex-direction: column; height: 100vh; overflow: auto;">
<View style="display: flex; height: 100%; gap: 10px;"> <View style="display: flex; height: 100%; gap: 10px;">
<View style="height: 100%; width: 85%; display: flex; flex-direction: column; gap: 5px;"> <View style="height: 100%; width: 85%; display: flex; flex-direction: column; gap: 5px;">
<Header value="WSI图像预览" /> <Header value="WSI图像预览" />
<View style="min-height: 100%;"> <View style="min-height: 100%;">
<Image name="image" value="$image" zoom="true" /> <Image name="image" value="$image" zoom="true" />
</View> </View>
</View> </View>
<View style="height: 100%; width: auto;"> <View style="height: 100%; width: auto;">
<View style="width: auto; display: flex;"> <View style="width: auto; display: flex;">
<Text name="case_id_title" toName="image" value="病例号: $case_id" /> <Text name="case_id_title" toName="image" value="病例号: $case_id" />
</View> </View>
<Text name="part_title" toName="image" value="取材部位: $part" /> <Text name="part_title" toName="image" value="取材部位: $part" />
<Header value="标注" /> <Header value="标注" />
<View style="display: flex; gap: 5px;"> <View style="display: flex; gap: 5px;">
<View> <View>
<Text name="cancer_or_not_title" value="是否有肿瘤" /> <Text name="cancer_or_not_title" value="是否有肿瘤" />
<Choices name="cancer_or_not" toName="image"> <Choices name="cancer_or_not" toName="image">
<Choice value="是" alias="1" /> <Choice value="是" alias="1" />
<Choice value="否" alias="0" /> <Choice value="否" alias="0" />
</Choices> </Choices>
<Text name="remark_title" value="备注" /> <Text name="remark_title" value="备注" />
<TextArea name="remark" toName="image" editable="true"/> <TextArea name="remark" toName="image" editable="true"/>
</View> </View>
</View> </View>
</View> </View>
</View> </View>
</View>`; </View>`;
const defaultTextTemplate = `<View style="display: flex; flex-direction: column; height: 100vh;"> const defaultTextTemplate = `<View style="display: flex; flex-direction: column; height: 100vh;">
<Header value="文本标注界面" /> <Header value="文本标注界面" />
<View style="display: flex; height: 100%; gap: 10px;"> <View style="display: flex; height: 100%; gap: 10px;">
<View style="flex: 1; padding: 10px;"> <View style="flex: 1; padding: 10px;">
<Text name="content" value="$text" /> <Text name="content" value="$text" />
<Labels name="label" toName="content"> <Labels name="label" toName="content">
<Label value="正面" background="green" /> <Label value="正面" background="green" />
<Label value="负面" background="red" /> <Label value="负面" background="red" />
<Label value="中性" background="gray" /> <Label value="中性" background="gray" />
</Labels> </Labels>
</View> </View>
<View style="width: 300px; padding: 10px; border-left: 1px solid #ccc;"> <View style="width: 300px; padding: 10px; border-left: 1px solid #ccc;">
<Header value="标注选项" /> <Header value="标注选项" />
<Text name="sentiment_title" value="情感分类" /> <Text name="sentiment_title" value="情感分类" />
<Choices name="sentiment" toName="content"> <Choices name="sentiment" toName="content">
<Choice value="正面" /> <Choice value="正面" />
<Choice value="负面" /> <Choice value="负面" />
<Choice value="中性" /> <Choice value="中性" />
</Choices> </Choices>
<Text name="confidence_title" value="置信度" /> <Text name="confidence_title" value="置信度" />
<Rating name="confidence" toName="content" maxRating="5" /> <Rating name="confidence" toName="content" maxRating="5" />
<Text name="comment_title" value="备注" /> <Text name="comment_title" value="备注" />
<TextArea name="comment" toName="content" placeholder="添加备注..." /> <TextArea name="comment" toName="content" placeholder="添加备注..." />
</View> </View>
</View> </View>
</View>`; </View>`;
const annotationTools = [ const annotationTools = [
{ id: "rectangle", label: "矩形框", icon: <BorderOutlined />, type: "image" }, { id: "rectangle", label: "矩形框", icon: <BorderOutlined />, type: "image" },
{ {
id: "polygon", id: "polygon",
label: "多边形", label: "多边形",
icon: <DeploymentUnitOutlined />, icon: <DeploymentUnitOutlined />,
type: "image", type: "image",
}, },
{ id: "circle", label: "圆形", icon: <DotChartOutlined />, type: "image" }, { id: "circle", label: "圆形", icon: <DotChartOutlined />, type: "image" },
{ id: "point", label: "关键点", icon: <AppstoreOutlined />, type: "image" }, { id: "point", label: "关键点", icon: <AppstoreOutlined />, type: "image" },
{ id: "text", label: "文本", icon: <EditOutlined />, type: "both" }, { id: "text", label: "文本", icon: <EditOutlined />, type: "both" },
{ id: "choices", label: "选择题", icon: <BarsOutlined />, type: "both" }, { id: "choices", label: "选择题", icon: <BarsOutlined />, type: "both" },
{ {
id: "checkbox", id: "checkbox",
label: "多选框", label: "多选框",
icon: <CheckSquareOutlined />, icon: <CheckSquareOutlined />,
type: "both", type: "both",
}, },
{ id: "textarea", label: "文本域", icon: <BarsOutlined />, type: "both" }, { id: "textarea", label: "文本域", icon: <BarsOutlined />, type: "both" },
]; ];
export default function CustomTemplateDialog({ export default function CustomTemplateDialog({
open, open,
onOpenChange, onOpenChange,
onSaveTemplate, onSaveTemplate,
datasetType, datasetType,
}: CustomTemplateDialogProps) { }: CustomTemplateDialogProps) {
const [templateName, setTemplateName] = useState(""); const [templateName, setTemplateName] = useState("");
const [templateDescription, setTemplateDescription] = useState(""); const [templateDescription, setTemplateDescription] = useState("");
const [templateCode, setTemplateCode] = useState( const [templateCode, setTemplateCode] = useState(
datasetType === "image" ? defaultImageTemplate : defaultTextTemplate datasetType === "image" ? defaultImageTemplate : defaultTextTemplate
); );
const handleSave = () => { const handleSave = () => {
if (!templateName.trim()) { if (!templateName.trim()) {
message.error("请输入模板名称"); message.error("请输入模板名称");
return; return;
} }
if (!templateCode.trim()) { if (!templateCode.trim()) {
message.error("请输入模板代码"); message.error("请输入模板代码");
return; return;
} }
const templateData = { const templateData = {
id: `custom-${Date.now()}`, id: `custom-${Date.now()}`,
name: templateName, name: templateName,
description: templateDescription, description: templateDescription,
code: templateCode, code: templateCode,
type: datasetType, type: datasetType,
isCustom: true, isCustom: true,
}; };
onSaveTemplate(templateData); onSaveTemplate(templateData);
onOpenChange(false); onOpenChange(false);
message.success("自定义模板已保存"); message.success("自定义模板已保存");
setTemplateName(""); setTemplateName("");
setTemplateDescription(""); setTemplateDescription("");
setTemplateCode( setTemplateCode(
datasetType === "image" ? defaultImageTemplate : defaultTextTemplate datasetType === "image" ? defaultImageTemplate : defaultTextTemplate
); );
}; };
return ( return (
<Modal <Modal
open={open} open={open}
onCancel={() => onOpenChange(false)} onCancel={() => onOpenChange(false)}
okText={"保存模板"} okText={"保存模板"}
onOk={handleSave} onOk={handleSave}
width={1200} width={1200}
className="max-h-[80vh] overflow-auto" className="max-h-[80vh] overflow-auto"
title="自定义标注模板" title="自定义标注模板"
> >
<div className="flex min-h-[500px]"> <div className="flex min-h-[500px]">
<div className="flex-1 pl-6"> <div className="flex-1 pl-6">
<Form layout="vertical"> <Form layout="vertical">
<Form.Item label="模板名称 *" required> <Form.Item label="模板名称 *" required>
<Input <Input
placeholder="输入模板名称" placeholder="输入模板名称"
value={templateName} value={templateName}
onChange={(e) => setTemplateName(e.target.value)} onChange={(e) => setTemplateName(e.target.value)}
/> />
</Form.Item> </Form.Item>
<Form.Item label="模板描述"> <Form.Item label="模板描述">
<Input <Input
placeholder="输入模板描述" placeholder="输入模板描述"
value={templateDescription} value={templateDescription}
onChange={(e) => setTemplateDescription(e.target.value)} onChange={(e) => setTemplateDescription(e.target.value)}
/> />
</Form.Item> </Form.Item>
</Form> </Form>
<div className="flex gap-6"> <div className="flex gap-6">
<div className="flex-1"> <div className="flex-1">
<div className="mb-2 font-medium"></div> <div className="mb-2 font-medium"></div>
<Card> <Card>
<TextArea <TextArea
rows={20} rows={20}
value={templateCode} value={templateCode}
onChange={(e) => setTemplateCode(e.target.value)} onChange={(e) => setTemplateCode(e.target.value)}
placeholder="输入模板代码" placeholder="输入模板代码"
/> />
</Card> </Card>
</div> </div>
<div className="w-96 border-l border-gray-100 pl-6"> <div className="w-96 border-l border-gray-100 pl-6">
<div className="mb-2 font-medium"></div> <div className="mb-2 font-medium"></div>
<Card <Card
cover={ cover={
<img <img
alt="预览图像" alt="预览图像"
src="https://hebbkx1anhila5yf.public.blob.vercel-storage.com/img_v3_02oi_9b855efe-ce37-4387-a845-d8ef9aaa1a8g.jpg-GhkhlenJlzOQLSDqyBm2iaC6jbv7VA.jpeg" src="https://hebbkx1anhila5yf.public.blob.vercel-storage.com/img_v3_02oi_9b855efe-ce37-4387-a845-d8ef9aaa1a8g.jpg-GhkhlenJlzOQLSDqyBm2iaC6jbv7VA.jpeg"
className="object-cover h-48" className="object-cover h-48"
/> />
} }
> >
<div className="mb-2"> <div className="mb-2">
<span className="text-gray-500"></span> <span className="text-gray-500"></span>
<span>undefined</span> <span>undefined</span>
</div> </div>
<div className="mb-2"> <div className="mb-2">
<span className="text-gray-500"></span> <span className="text-gray-500"></span>
<span>undefined</span> <span>undefined</span>
</div> </div>
<Divider /> <Divider />
<div> <div>
<div className="font-medium mb-2"></div> <div className="font-medium mb-2"></div>
<div className="mb-2 text-gray-500"></div> <div className="mb-2 text-gray-500"></div>
<Radio.Group> <Radio.Group>
<Radio value="1">[1]</Radio> <Radio value="1">[1]</Radio>
<Radio value="0">[2]</Radio> <Radio value="0">[2]</Radio>
</Radio.Group> </Radio.Group>
<div className="mt-4"> <div className="mt-4">
<div className="text-gray-500 mb-1"></div> <div className="text-gray-500 mb-1"></div>
<TextArea rows={3} placeholder="添加备注..." /> <TextArea rows={3} placeholder="添加备注..." />
</div> </div>
</div> </div>
</Card> </Card>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</Modal> </Modal>
); );
} }

View File

@@ -1,187 +1,187 @@
import { Button, Card, Input, InputNumber, Popconfirm, Select, Switch, Tooltip } from "antd"; import { Button, Card, Input, InputNumber, Popconfirm, Select, Switch, Tooltip } from "antd";
import { PlusOutlined, DeleteOutlined } from "@ant-design/icons"; import { PlusOutlined, DeleteOutlined } from "@ant-design/icons";
import { useState, useImperativeHandle, forwardRef } from "react"; import { useState, useImperativeHandle, forwardRef } from "react";
type LabelType = "string" | "number" | "enum"; type LabelType = "string" | "number" | "enum";
type LabelItem = { type LabelItem = {
id: string; id: string;
name: string; name: string;
type: LabelType; type: LabelType;
required?: boolean; required?: boolean;
// for enum: values; for number: min/max // for enum: values; for number: min/max
values?: string[]; values?: string[];
min?: number | null; min?: number | null;
max?: number | null; max?: number | null;
step?: number | null; step?: number | null;
}; };
type LabelingConfigEditorProps = { type LabelingConfigEditorProps = {
initial?: any; initial?: any;
onGenerate: (config: any) => void; onGenerate: (config: any) => void;
hideFooter?: boolean; hideFooter?: boolean;
}; };
export default forwardRef<any, LabelingConfigEditorProps>(function LabelingConfigEditor( export default forwardRef<any, LabelingConfigEditorProps>(function LabelingConfigEditor(
{ initial, onGenerate, hideFooter }: LabelingConfigEditorProps, { initial, onGenerate, hideFooter }: LabelingConfigEditorProps,
ref: any ref: any
) { ) {
const [labels, setLabels] = useState<LabelItem[]>(() => { const [labels, setLabels] = useState<LabelItem[]>(() => {
if (initial && initial.labels && Array.isArray(initial.labels)) { if (initial && initial.labels && Array.isArray(initial.labels)) {
return initial.labels.map((l: any, idx: number) => ({ return initial.labels.map((l: any, idx: number) => ({
id: `${Date.now()}-${idx}`, id: `${Date.now()}-${idx}`,
name: l.name || "", name: l.name || "",
type: l.type || "string", type: l.type || "string",
required: !!l.required, required: !!l.required,
values: l.values || (l.type === "enum" ? [] : undefined), values: l.values || (l.type === "enum" ? [] : undefined),
min: l.min ?? null, min: l.min ?? null,
max: l.max ?? null, max: l.max ?? null,
})); }));
} }
return []; return [];
}); });
const addLabel = () => { const addLabel = () => {
setLabels((s) => [ setLabels((s) => [
...s, ...s,
{ id: `${Date.now()}-${Math.random()}`, name: "", type: "string", required: false, step: null }, { id: `${Date.now()}-${Math.random()}`, name: "", type: "string", required: false, step: null },
]); ]);
}; };
const updateLabel = (id: string, patch: Partial<LabelItem>) => { const updateLabel = (id: string, patch: Partial<LabelItem>) => {
setLabels((s) => s.map((l) => (l.id === id ? { ...l, ...patch } : l))); setLabels((s) => s.map((l) => (l.id === id ? { ...l, ...patch } : l)));
}; };
const removeLabel = (id: string) => { const removeLabel = (id: string) => {
setLabels((s) => s.filter((l) => l.id !== id)); setLabels((s) => s.filter((l) => l.id !== id));
}; };
const generate = () => { const generate = () => {
// basic validation: label name non-empty // basic validation: label name non-empty
for (const l of labels) { for (const l of labels) {
if (!l.name || l.name.trim() === "") { if (!l.name || l.name.trim() === "") {
// focus not available here, just abort // focus not available here, just abort
// Could show a more friendly UI; keep simple for now // Could show a more friendly UI; keep simple for now
// eslint-disable-next-line no-alert // eslint-disable-next-line no-alert
alert("请为所有标签填写名称"); alert("请为所有标签填写名称");
return; return;
} }
if (l.type === "enum") { if (l.type === "enum") {
if (!l.values || l.values.length === 0) { if (!l.values || l.values.length === 0) {
alert(`枚举标签 ${l.name} 需要至少一个取值`); alert(`枚举标签 ${l.name} 需要至少一个取值`);
return; return;
} }
} }
if (l.type === "number") { if (l.type === "number") {
// validate min/max // validate min/max
if (l.min != null && l.max != null && l.min > l.max) { if (l.min != null && l.max != null && l.min > l.max) {
alert(`数值标签 ${l.name} 的最小值不能大于最大值`); alert(`数值标签 ${l.name} 的最小值不能大于最大值`);
return; return;
} }
// validate step // validate step
if (l.step != null && (!(typeof l.step === "number") || l.step <= 0)) { if (l.step != null && (!(typeof l.step === "number") || l.step <= 0)) {
alert(`数值标签 ${l.name} 的步长必须为大于 0 的数值`); alert(`数值标签 ${l.name} 的步长必须为大于 0 的数值`);
return; return;
} }
} }
} }
const config = { const config = {
labels: labels.map((l) => { labels: labels.map((l) => {
const item: any = { name: l.name, type: l.type, required: !!l.required }; const item: any = { name: l.name, type: l.type, required: !!l.required };
if (l.type === "enum") item.values = l.values || []; if (l.type === "enum") item.values = l.values || [];
if (l.type === "number") { if (l.type === "number") {
if (l.min != null) item.min = l.min; if (l.min != null) item.min = l.min;
if (l.max != null) item.max = l.max; if (l.max != null) item.max = l.max;
} }
return item; return item;
}), }),
}; };
onGenerate(config); onGenerate(config);
}; };
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
addLabel, addLabel,
generate, generate,
})); }));
return ( return (
<div> <div>
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}> <div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
{labels.map((label) => ( {labels.map((label) => (
<Card size="small" key={label.id} styles={{ body: { padding: 10 } }}> <Card size="small" key={label.id} styles={{ body: { padding: 10 } }}>
<div style={{ display: "flex", flexWrap: "wrap", gap: 8, alignItems: "center" }}> <div style={{ display: "flex", flexWrap: "wrap", gap: 8, alignItems: "center" }}>
<Input <Input
placeholder="标签名称" placeholder="标签名称"
value={label.name} value={label.name}
onChange={(e) => updateLabel(label.id, { name: e.target.value })} onChange={(e) => updateLabel(label.id, { name: e.target.value })}
style={{ flex: "1 1 160px", minWidth: 120 }} style={{ flex: "1 1 160px", minWidth: 120 }}
/> />
<Select <Select
value={label.type} value={label.type}
onChange={(v) => updateLabel(label.id, { type: v as LabelType })} onChange={(v) => updateLabel(label.id, { type: v as LabelType })}
options={[{ label: "文本", value: "string" }, { label: "数值", value: "number" }, { label: "枚举", value: "enum" }]} options={[{ label: "文本", value: "string" }, { label: "数值", value: "number" }, { label: "枚举", value: "enum" }]}
style={{ width: 120, flex: "0 0 120px" }} style={{ width: 120, flex: "0 0 120px" }}
/> />
{label.type === "enum" && ( {label.type === "enum" && (
<Input.TextArea <Input.TextArea
placeholder="每行一个枚举值,按回车换行" placeholder="每行一个枚举值,按回车换行"
value={(label.values || []).join("\n")} value={(label.values || []).join("\n")}
onChange={(e) => updateLabel(label.id, { values: e.target.value.split(/\r?\n/).map((s) => s.trim()).filter(Boolean) })} onChange={(e) => updateLabel(label.id, { values: e.target.value.split(/\r?\n/).map((s) => s.trim()).filter(Boolean) })}
onKeyDown={(e) => { onKeyDown={(e) => {
// Prevent parent handlers (like Form submit or modal shortcuts) from intercepting Enter // Prevent parent handlers (like Form submit or modal shortcuts) from intercepting Enter
e.stopPropagation(); e.stopPropagation();
}} }}
rows={3} rows={3}
style={{ flex: "1 1 220px", minWidth: 160, width: "100%", resize: "vertical" }} style={{ flex: "1 1 220px", minWidth: 160, width: "100%", resize: "vertical" }}
/> />
)} )}
{label.type === "number" && ( {label.type === "number" && (
<div style={{ display: "flex", gap: 8, alignItems: "center", flex: "0 0 auto" }}> <div style={{ display: "flex", gap: 8, alignItems: "center", flex: "0 0 auto" }}>
<Tooltip title="最小值"> <Tooltip title="最小值">
<InputNumber value={label.min ?? null} onChange={(v) => updateLabel(label.id, { min: v ?? null })} placeholder="min" /> <InputNumber value={label.min ?? null} onChange={(v) => updateLabel(label.id, { min: v ?? null })} placeholder="min" />
</Tooltip> </Tooltip>
<Tooltip title="最大值"> <Tooltip title="最大值">
<InputNumber value={label.max ?? null} onChange={(v) => updateLabel(label.id, { max: v ?? null })} placeholder="max" /> <InputNumber value={label.max ?? null} onChange={(v) => updateLabel(label.id, { max: v ?? null })} placeholder="max" />
</Tooltip> </Tooltip>
<Tooltip title="步长 (step)"> <Tooltip title="步长 (step)">
<InputNumber value={label.step ?? null} onChange={(v) => updateLabel(label.id, { step: v ?? null })} placeholder="step" min={0} /> <InputNumber value={label.step ?? null} onChange={(v) => updateLabel(label.id, { step: v ?? null })} placeholder="step" min={0} />
</Tooltip> </Tooltip>
</div> </div>
)} )}
<div style={{ display: "flex", alignItems: "center", gap: 8, marginLeft: "auto" }}> <div style={{ display: "flex", alignItems: "center", gap: 8, marginLeft: "auto" }}>
<span style={{ fontSize: 12, color: "rgba(0,0,0,0.65)" }}></span> <span style={{ fontSize: 12, color: "rgba(0,0,0,0.65)" }}></span>
<Switch checked={!!label.required} onChange={(v) => updateLabel(label.id, { required: v })} /> <Switch checked={!!label.required} onChange={(v) => updateLabel(label.id, { required: v })} />
<Popconfirm title="确认删除该标签?" onConfirm={() => removeLabel(label.id)}> <Popconfirm title="确认删除该标签?" onConfirm={() => removeLabel(label.id)}>
<Button type="text" icon={<DeleteOutlined />} /> <Button type="text" icon={<DeleteOutlined />} />
</Popconfirm> </Popconfirm>
</div> </div>
</div> </div>
<div style={{ marginTop: 8, color: "rgba(0,0,0,0.45)", fontSize: 12 }}> <div style={{ marginTop: 8, color: "rgba(0,0,0,0.45)", fontSize: 12 }}>
{label.type === "string" && <span></span>} {label.type === "string" && <span></span>}
{label.type === "number" && <span> min / max / step</span>} {label.type === "number" && <span> min / max / step</span>}
{label.type === "enum" && <span></span>} {label.type === "enum" && <span></span>}
</div> </div>
</Card> </Card>
))} ))}
{!hideFooter && ( {!hideFooter && (
<div style={{ display: "flex", gap: 8 }}> <div style={{ display: "flex", gap: 8 }}>
<Button icon={<PlusOutlined />} onClick={addLabel}> <Button icon={<PlusOutlined />} onClick={addLabel}>
</Button> </Button>
<Button type="primary" onClick={generate}> <Button type="primary" onClick={generate}>
JSON JSON
</Button> </Button>
</div> </div>
)} )}
</div> </div>
</div> </div>
); );
} }
); );

View File

@@ -1,398 +1,398 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { Card, Button, Table, message, Modal, Tabs } from "antd"; import { Card, Button, Table, message, Modal, Tabs } from "antd";
import { import {
PlusOutlined, PlusOutlined,
EditOutlined, EditOutlined,
DeleteOutlined, DeleteOutlined,
SyncOutlined, SyncOutlined,
} from "@ant-design/icons"; } from "@ant-design/icons";
import { SearchControls } from "@/components/SearchControls"; import { SearchControls } from "@/components/SearchControls";
import CardView from "@/components/CardView"; import CardView from "@/components/CardView";
import type { AnnotationTask } from "../annotation.model"; import type { AnnotationTask } from "../annotation.model";
import useFetchData from "@/hooks/useFetchData"; import useFetchData from "@/hooks/useFetchData";
import { import {
deleteAnnotationTaskByIdUsingDelete, loginAnnotationUsingGet, deleteAnnotationTaskByIdUsingDelete, loginAnnotationUsingGet,
queryAnnotationTasksUsingGet, queryAnnotationTasksUsingGet,
syncAnnotationTaskUsingPost, syncAnnotationTaskUsingPost,
} from "../annotation.api"; } from "../annotation.api";
import { mapAnnotationTask } from "../annotation.const"; import { mapAnnotationTask } from "../annotation.const";
import CreateAnnotationTask from "../Create/components/CreateAnnotationTaskDialog"; import CreateAnnotationTask from "../Create/components/CreateAnnotationTaskDialog";
import { ColumnType } from "antd/es/table"; import { ColumnType } from "antd/es/table";
import { TemplateList } from "../Template"; import { TemplateList } from "../Template";
// Note: DevelopmentInProgress intentionally not used here // Note: DevelopmentInProgress intentionally not used here
export default function DataAnnotation() { export default function DataAnnotation() {
// return <DevelopmentInProgress showTime="2025.10.30" />; // return <DevelopmentInProgress showTime="2025.10.30" />;
const [activeTab, setActiveTab] = useState("tasks"); const [activeTab, setActiveTab] = useState("tasks");
const [viewMode, setViewMode] = useState<"list" | "card">("list"); const [viewMode, setViewMode] = useState<"list" | "card">("list");
const [showCreateDialog, setShowCreateDialog] = useState(false); const [showCreateDialog, setShowCreateDialog] = useState(false);
const { const {
loading, loading,
tableData, tableData,
pagination, pagination,
searchParams, searchParams,
fetchData, fetchData,
handleFiltersChange, handleFiltersChange,
handleKeywordChange, handleKeywordChange,
} = useFetchData(queryAnnotationTasksUsingGet, mapAnnotationTask, 30000, true, [], 0); } = useFetchData(queryAnnotationTasksUsingGet, mapAnnotationTask, 30000, true, [], 0);
const [labelStudioBase, setLabelStudioBase] = useState<string | null>(null); const [labelStudioBase, setLabelStudioBase] = useState<string | null>(null);
const [selectedRowKeys, setSelectedRowKeys] = useState<(string | number)[]>([]); const [selectedRowKeys, setSelectedRowKeys] = useState<(string | number)[]>([]);
const [selectedRows, setSelectedRows] = useState<any[]>([]); const [selectedRows, setSelectedRows] = useState<any[]>([]);
// prefetch config on mount so clicking annotate is fast and we know whether base URL exists // prefetch config on mount so clicking annotate is fast and we know whether base URL exists
// useEffect ensures this runs once // useEffect ensures this runs once
useEffect(() => { useEffect(() => {
let mounted = true; let mounted = true;
(async () => { (async () => {
try { try {
const baseUrl = `http://${window.location.hostname}:${parseInt(window.location.port) + 1}`; const baseUrl = `http://${window.location.hostname}:${parseInt(window.location.port) + 1}`;
if (mounted) setLabelStudioBase(baseUrl); if (mounted) setLabelStudioBase(baseUrl);
} catch (e) { } catch (e) {
if (mounted) setLabelStudioBase(null); if (mounted) setLabelStudioBase(null);
} }
})(); })();
return () => { return () => {
mounted = false; mounted = false;
}; };
}, []); }, []);
const handleAnnotate = (task: AnnotationTask) => { const handleAnnotate = (task: AnnotationTask) => {
// Open Label Studio project page in a new tab // Open Label Studio project page in a new tab
(async () => { (async () => {
try { try {
// prefer using labeling project id already present on the task // prefer using labeling project id already present on the task
// `mapAnnotationTask` normalizes upstream fields into `labelingProjId`/`projId`, // `mapAnnotationTask` normalizes upstream fields into `labelingProjId`/`projId`,
// so prefer those and fall back to the task id if necessary. // so prefer those and fall back to the task id if necessary.
let labelingProjId = (task as any).labelingProjId || (task as any).projId || undefined; let labelingProjId = (task as any).labelingProjId || (task as any).projId || undefined;
// no fallback external mapping lookup; rely on normalized fields from mapAnnotationTask // no fallback external mapping lookup; rely on normalized fields from mapAnnotationTask
// use prefetched base if available // use prefetched base if available
const base = labelStudioBase; const base = labelStudioBase;
// no debug logging in production // no debug logging in production
if (labelingProjId) { if (labelingProjId) {
// only open external Label Studio when we have a configured base url // only open external Label Studio when we have a configured base url
await loginAnnotationUsingGet(labelingProjId) await loginAnnotationUsingGet(labelingProjId)
if (base) { if (base) {
const target = `${base}/projects/${labelingProjId}/data`; const target = `${base}/projects/${labelingProjId}/data`;
window.open(target, "_blank"); window.open(target, "_blank");
} else { } else {
// no external Label Studio URL configured — do not perform internal redirect in this version // no external Label Studio URL configured — do not perform internal redirect in this version
message.error("无法跳转到 Label Studio:未配置 Label Studio 基础 URL"); message.error("无法跳转到 Label Studio:未配置 Label Studio 基础 URL");
return; return;
} }
} else { } else {
// no labeling project id available — do not attempt internal redirect in this version // no labeling project id available — do not attempt internal redirect in this version
message.error("无法跳转到 Label Studio:该映射未绑定标注项目"); message.error("无法跳转到 Label Studio:该映射未绑定标注项目");
return; return;
} }
} catch (error) { } catch (error) {
// on error, surface a user-friendly message instead of redirecting // on error, surface a user-friendly message instead of redirecting
message.error("无法跳转到 Label Studio:发生错误,请检查配置或控制台日志"); message.error("无法跳转到 Label Studio:发生错误,请检查配置或控制台日志");
return; return;
} }
})(); })();
}; };
const handleDelete = (task: AnnotationTask) => { const handleDelete = (task: AnnotationTask) => {
Modal.confirm({ Modal.confirm({
title: `确认删除标注任务「${task.name}」吗?`, title: `确认删除标注任务「${task.name}」吗?`,
content: ( content: (
<div> <div>
<div></div> <div></div>
<div></div> <div></div>
</div> </div>
), ),
okText: "删除", okText: "删除",
okType: "danger", okType: "danger",
cancelText: "取消", cancelText: "取消",
onOk: async () => { onOk: async () => {
try { try {
await deleteAnnotationTaskByIdUsingDelete(task.id); await deleteAnnotationTaskByIdUsingDelete(task.id);
message.success("映射删除成功"); message.success("映射删除成功");
fetchData(); fetchData();
// clear selection if deleted item was selected // clear selection if deleted item was selected
setSelectedRowKeys((keys) => keys.filter((k) => k !== task.id)); setSelectedRowKeys((keys) => keys.filter((k) => k !== task.id));
setSelectedRows((rows) => rows.filter((r) => r.id !== task.id)); setSelectedRows((rows) => rows.filter((r) => r.id !== task.id));
} catch (e) { } catch (e) {
console.error(e); console.error(e);
message.error("删除失败,请稍后重试"); message.error("删除失败,请稍后重试");
} }
}, },
}); });
}; };
const handleSync = (task: AnnotationTask, batchSize: number = 50) => { const handleSync = (task: AnnotationTask, batchSize: number = 50) => {
Modal.confirm({ Modal.confirm({
title: `确认同步标注任务「${task.name}」吗?`, title: `确认同步标注任务「${task.name}」吗?`,
content: ( content: (
<div> <div>
<div></div> <div></div>
<div></div> <div></div>
</div> </div>
), ),
okText: "同步", okText: "同步",
cancelText: "取消", cancelText: "取消",
onOk: async () => { onOk: async () => {
try { try {
await syncAnnotationTaskUsingPost({ id: task.id, batchSize }); await syncAnnotationTaskUsingPost({ id: task.id, batchSize });
message.success("任务同步请求已发送"); message.success("任务同步请求已发送");
// optional: refresh list/status // optional: refresh list/status
fetchData(); fetchData();
// clear selection for the task // clear selection for the task
setSelectedRowKeys((keys) => keys.filter((k) => k !== task.id)); setSelectedRowKeys((keys) => keys.filter((k) => k !== task.id));
setSelectedRows((rows) => rows.filter((r) => r.id !== task.id)); setSelectedRows((rows) => rows.filter((r) => r.id !== task.id));
} catch (e) { } catch (e) {
console.error(e); console.error(e);
message.error("同步失败,请稍后重试"); message.error("同步失败,请稍后重试");
} }
}, },
}); });
}; };
const handleBatchSync = (batchSize: number = 50) => { const handleBatchSync = (batchSize: number = 50) => {
if (!selectedRows || selectedRows.length === 0) return; if (!selectedRows || selectedRows.length === 0) return;
Modal.confirm({ Modal.confirm({
title: `确认同步所选 ${selectedRows.length} 个标注任务吗?`, title: `确认同步所选 ${selectedRows.length} 个标注任务吗?`,
content: ( content: (
<div> <div>
<div></div> <div></div>
<div></div> <div></div>
</div> </div>
), ),
okText: "同步", okText: "同步",
cancelText: "取消", cancelText: "取消",
onOk: async () => { onOk: async () => {
try { try {
await Promise.all( await Promise.all(
selectedRows.map((r) => syncAnnotationTaskUsingPost({ id: r.id, batchSize })) selectedRows.map((r) => syncAnnotationTaskUsingPost({ id: r.id, batchSize }))
); );
message.success("批量同步请求已发送"); message.success("批量同步请求已发送");
fetchData(); fetchData();
setSelectedRowKeys([]); setSelectedRowKeys([]);
setSelectedRows([]); setSelectedRows([]);
} catch (e) { } catch (e) {
console.error(e); console.error(e);
message.error("批量同步失败,请稍后重试"); message.error("批量同步失败,请稍后重试");
} }
}, },
}); });
}; };
const handleBatchDelete = () => { const handleBatchDelete = () => {
if (!selectedRows || selectedRows.length === 0) return; if (!selectedRows || selectedRows.length === 0) return;
Modal.confirm({ Modal.confirm({
title: `确认删除所选 ${selectedRows.length} 个标注任务吗?`, title: `确认删除所选 ${selectedRows.length} 个标注任务吗?`,
content: ( content: (
<div> <div>
<div></div> <div></div>
<div></div> <div></div>
</div> </div>
), ),
okText: "删除", okText: "删除",
okType: "danger", okType: "danger",
cancelText: "取消", cancelText: "取消",
onOk: async () => { onOk: async () => {
try { try {
await Promise.all( await Promise.all(
selectedRows.map((r) => deleteAnnotationTaskByIdUsingDelete(r.id)) selectedRows.map((r) => deleteAnnotationTaskByIdUsingDelete(r.id))
); );
message.success("批量删除已完成"); message.success("批量删除已完成");
fetchData(); fetchData();
setSelectedRowKeys([]); setSelectedRowKeys([]);
setSelectedRows([]); setSelectedRows([]);
} catch (e) { } catch (e) {
console.error(e); console.error(e);
message.error("批量删除失败,请稍后重试"); message.error("批量删除失败,请稍后重试");
} }
}, },
}); });
}; };
const operations = [ const operations = [
{ {
key: "annotate", key: "annotate",
label: "标注", label: "标注",
icon: ( icon: (
<EditOutlined <EditOutlined
className="w-4 h-4 text-green-400" className="w-4 h-4 text-green-400"
style={{ color: "#52c41a" }} style={{ color: "#52c41a" }}
/> />
), ),
onClick: handleAnnotate, onClick: handleAnnotate,
}, },
{ {
key: "sync", key: "sync",
label: "同步", label: "同步",
icon: <SyncOutlined className="w-4 h-4" style={{ color: "#722ed1" }} />, icon: <SyncOutlined className="w-4 h-4" style={{ color: "#722ed1" }} />,
onClick: handleSync, onClick: handleSync,
}, },
{ {
key: "delete", key: "delete",
label: "删除", label: "删除",
icon: <DeleteOutlined style={{ color: "#f5222d" }} />, icon: <DeleteOutlined style={{ color: "#f5222d" }} />,
onClick: handleDelete, onClick: handleDelete,
}, },
]; ];
const columns: ColumnType<any>[] = [ const columns: ColumnType<any>[] = [
{ {
title: "任务名称", title: "任务名称",
dataIndex: "name", dataIndex: "name",
key: "name", key: "name",
fixed: "left" as const, fixed: "left" as const,
}, },
{ {
title: "任务ID", title: "任务ID",
dataIndex: "id", dataIndex: "id",
key: "id", key: "id",
}, },
{ {
title: "数据集", title: "数据集",
dataIndex: "datasetName", dataIndex: "datasetName",
key: "datasetName", key: "datasetName",
width: 180, width: 180,
}, },
{ {
title: "创建时间", title: "创建时间",
dataIndex: "createdAt", dataIndex: "createdAt",
key: "createdAt", key: "createdAt",
width: 180, width: 180,
}, },
{ {
title: "更新时间", title: "更新时间",
dataIndex: "updatedAt", dataIndex: "updatedAt",
key: "updatedAt", key: "updatedAt",
width: 180, width: 180,
}, },
{ {
title: "操作", title: "操作",
key: "actions", key: "actions",
fixed: "right" as const, fixed: "right" as const,
width: 150, width: 150,
dataIndex: "actions", dataIndex: "actions",
render: (_: any, task: any) => ( render: (_: any, task: any) => (
<div className="flex items-center justify-center space-x-1"> <div className="flex items-center justify-center space-x-1">
{operations.map((operation) => ( {operations.map((operation) => (
<Button <Button
key={operation.key} key={operation.key}
type="text" type="text"
icon={operation.icon} icon={operation.icon}
onClick={() => (operation?.onClick as any)?.(task)} onClick={() => (operation?.onClick as any)?.(task)}
title={operation.label} title={operation.label}
/> />
))} ))}
</div> </div>
), ),
}, },
]; ];
return ( return (
<div className="flex flex-col h-full gap-4"> <div className="flex flex-col h-full gap-4">
{/* Header */} {/* Header */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h1 className="text-xl font-bold"></h1> <h1 className="text-xl font-bold"></h1>
</div> </div>
{/* Tabs */} {/* Tabs */}
<Tabs <Tabs
activeKey={activeTab} activeKey={activeTab}
onChange={setActiveTab} onChange={setActiveTab}
items={[ items={[
{ {
key: "tasks", key: "tasks",
label: "标注任务", label: "标注任务",
children: ( children: (
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
{/* Search, Filters and Buttons in one row */} {/* Search, Filters and Buttons in one row */}
<div className="flex items-center justify-between gap-2"> <div className="flex items-center justify-between gap-2">
{/* Left side: Search and view controls */} {/* Left side: Search and view controls */}
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<SearchControls <SearchControls
searchTerm={searchParams.keyword} searchTerm={searchParams.keyword}
onSearchChange={handleKeywordChange} onSearchChange={handleKeywordChange}
searchPlaceholder="搜索任务名称、描述" searchPlaceholder="搜索任务名称、描述"
onFiltersChange={handleFiltersChange} onFiltersChange={handleFiltersChange}
viewMode={viewMode} viewMode={viewMode}
onViewModeChange={setViewMode} onViewModeChange={setViewMode}
showViewToggle={true} showViewToggle={true}
onReload={fetchData} onReload={fetchData}
/> />
</div> </div>
{/* Right side: All action buttons */} {/* Right side: All action buttons */}
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Button <Button
onClick={() => handleBatchSync(50)} onClick={() => handleBatchSync(50)}
disabled={selectedRowKeys.length === 0} disabled={selectedRowKeys.length === 0}
> >
</Button> </Button>
<Button <Button
danger danger
onClick={handleBatchDelete} onClick={handleBatchDelete}
disabled={selectedRowKeys.length === 0} disabled={selectedRowKeys.length === 0}
> >
</Button> </Button>
<Button <Button
type="primary" type="primary"
icon={<PlusOutlined />} icon={<PlusOutlined />}
onClick={() => setShowCreateDialog(true)} onClick={() => setShowCreateDialog(true)}
> >
</Button> </Button>
</div> </div>
</div> </div>
{/* Task List/Card */} {/* Task List/Card */}
{viewMode === "list" ? ( {viewMode === "list" ? (
<Card> <Card>
<Table <Table
key="id" key="id"
rowKey="id" rowKey="id"
loading={loading} loading={loading}
columns={columns} columns={columns}
dataSource={tableData} dataSource={tableData}
pagination={pagination} pagination={pagination}
rowSelection={{ rowSelection={{
selectedRowKeys, selectedRowKeys,
onChange: (keys, rows) => { onChange: (keys, rows) => {
setSelectedRowKeys(keys as (string | number)[]); setSelectedRowKeys(keys as (string | number)[]);
setSelectedRows(rows as any[]); setSelectedRows(rows as any[]);
}, },
}} }}
scroll={{ x: "max-content", y: "calc(100vh - 24rem)" }} scroll={{ x: "max-content", y: "calc(100vh - 24rem)" }}
/> />
</Card> </Card>
) : ( ) : (
<CardView <CardView
data={tableData} data={tableData}
operations={operations as any} operations={operations as any}
pagination={pagination} pagination={pagination}
loading={loading} loading={loading}
/> />
)} )}
<CreateAnnotationTask <CreateAnnotationTask
open={showCreateDialog} open={showCreateDialog}
onClose={() => setShowCreateDialog(false)} onClose={() => setShowCreateDialog(false)}
onRefresh={fetchData} onRefresh={fetchData}
/> />
</div> </div>
), ),
}, },
{ {
key: "templates", key: "templates",
label: "标注模板", label: "标注模板",
children: <TemplateList />, children: <TemplateList />,
}, },
]} ]}
/> />
</div> </div>
); );
} }

View File

@@ -1,155 +1,155 @@
import React from "react"; import React from "react";
import { Modal, Descriptions, Tag, Space, Divider, Card, Typography } from "antd"; import { Modal, Descriptions, Tag, Space, Divider, Card, Typography } from "antd";
import type { AnnotationTemplate } from "../annotation.model"; import type { AnnotationTemplate } from "../annotation.model";
const { Text, Paragraph } = Typography; const { Text, Paragraph } = Typography;
interface TemplateDetailProps { interface TemplateDetailProps {
visible: boolean; visible: boolean;
template?: AnnotationTemplate; template?: AnnotationTemplate;
onClose: () => void; onClose: () => void;
} }
const TemplateDetail: React.FC<TemplateDetailProps> = ({ const TemplateDetail: React.FC<TemplateDetailProps> = ({
visible, visible,
template, template,
onClose, onClose,
}) => { }) => {
if (!template) return null; if (!template) return null;
return ( return (
<Modal <Modal
title="模板详情" title="模板详情"
open={visible} open={visible}
onCancel={onClose} onCancel={onClose}
footer={null} footer={null}
width={800} width={800}
> >
<Descriptions bordered column={2}> <Descriptions bordered column={2}>
<Descriptions.Item label="名称" span={2}> <Descriptions.Item label="名称" span={2}>
{template.name} {template.name}
</Descriptions.Item> </Descriptions.Item>
<Descriptions.Item label="描述" span={2}> <Descriptions.Item label="描述" span={2}>
{template.description || "-"} {template.description || "-"}
</Descriptions.Item> </Descriptions.Item>
<Descriptions.Item label="数据类型"> <Descriptions.Item label="数据类型">
<Tag color="cyan">{template.dataType}</Tag> <Tag color="cyan">{template.dataType}</Tag>
</Descriptions.Item> </Descriptions.Item>
<Descriptions.Item label="标注类型"> <Descriptions.Item label="标注类型">
<Tag color="geekblue">{template.labelingType}</Tag> <Tag color="geekblue">{template.labelingType}</Tag>
</Descriptions.Item> </Descriptions.Item>
<Descriptions.Item label="分类"> <Descriptions.Item label="分类">
<Tag color="blue">{template.category}</Tag> <Tag color="blue">{template.category}</Tag>
</Descriptions.Item> </Descriptions.Item>
<Descriptions.Item label="样式"> <Descriptions.Item label="样式">
{template.style} {template.style}
</Descriptions.Item> </Descriptions.Item>
<Descriptions.Item label="类型"> <Descriptions.Item label="类型">
<Tag color={template.builtIn ? "gold" : "default"}> <Tag color={template.builtIn ? "gold" : "default"}>
{template.builtIn ? "系统内置" : "自定义"} {template.builtIn ? "系统内置" : "自定义"}
</Tag> </Tag>
</Descriptions.Item> </Descriptions.Item>
<Descriptions.Item label="版本"> <Descriptions.Item label="版本">
{template.version} {template.version}
</Descriptions.Item> </Descriptions.Item>
<Descriptions.Item label="创建时间" span={2}> <Descriptions.Item label="创建时间" span={2}>
{new Date(template.createdAt).toLocaleString()} {new Date(template.createdAt).toLocaleString()}
</Descriptions.Item> </Descriptions.Item>
{template.updatedAt && ( {template.updatedAt && (
<Descriptions.Item label="更新时间" span={2}> <Descriptions.Item label="更新时间" span={2}>
{new Date(template.updatedAt).toLocaleString()} {new Date(template.updatedAt).toLocaleString()}
</Descriptions.Item> </Descriptions.Item>
)} )}
</Descriptions> </Descriptions>
<Divider></Divider> <Divider></Divider>
<Card title="数据对象" size="small" style={{ marginBottom: 16 }}> <Card title="数据对象" size="small" style={{ marginBottom: 16 }}>
<Space direction="vertical" style={{ width: "100%" }}> <Space direction="vertical" style={{ width: "100%" }}>
{template.configuration.objects.map((obj, index) => ( {template.configuration.objects.map((obj, index) => (
<Card key={index} size="small" type="inner"> <Card key={index} size="small" type="inner">
<Space> <Space>
<Text strong></Text> <Text strong></Text>
<Tag>{obj.name}</Tag> <Tag>{obj.name}</Tag>
<Text strong></Text> <Text strong></Text>
<Tag color="blue">{obj.type}</Tag> <Tag color="blue">{obj.type}</Tag>
<Text strong></Text> <Text strong></Text>
<Tag color="green">{obj.value}</Tag> <Tag color="green">{obj.value}</Tag>
</Space> </Space>
</Card> </Card>
))} ))}
</Space> </Space>
</Card> </Card>
<Card title="标注控件" size="small" style={{ marginBottom: 16 }}> <Card title="标注控件" size="small" style={{ marginBottom: 16 }}>
<Space direction="vertical" style={{ width: "100%" }} size="middle"> <Space direction="vertical" style={{ width: "100%" }} size="middle">
{template.configuration.labels.map((label, index) => ( {template.configuration.labels.map((label, index) => (
<Card key={index} size="small" type="inner" title={`控件 ${index + 1}`}> <Card key={index} size="small" type="inner" title={`控件 ${index + 1}`}>
<Space direction="vertical" style={{ width: "100%" }}> <Space direction="vertical" style={{ width: "100%" }}>
<div> <div>
<Text strong></Text> <Text strong></Text>
<Tag>{label.fromName}</Tag> <Tag>{label.fromName}</Tag>
<Text strong style={{ marginLeft: 16 }}></Text> <Text strong style={{ marginLeft: 16 }}></Text>
<Tag>{label.toName}</Tag> <Tag>{label.toName}</Tag>
<Text strong style={{ marginLeft: 16 }}></Text> <Text strong style={{ marginLeft: 16 }}></Text>
<Tag color="purple">{label.type}</Tag> <Tag color="purple">{label.type}</Tag>
{label.required && <Tag color="red"></Tag>} {label.required && <Tag color="red"></Tag>}
</div> </div>
{label.description && ( {label.description && (
<div> <div>
<Text strong></Text> <Text strong></Text>
<Text type="secondary">{label.description}</Text> <Text type="secondary">{label.description}</Text>
</div> </div>
)} )}
{label.options && label.options.length > 0 && ( {label.options && label.options.length > 0 && (
<div> <div>
<Text strong></Text> <Text strong></Text>
<div style={{ marginTop: 4 }}> <div style={{ marginTop: 4 }}>
{label.options.map((opt, i) => ( {label.options.map((opt, i) => (
<Tag key={i} color="cyan">{opt}</Tag> <Tag key={i} color="cyan">{opt}</Tag>
))} ))}
</div> </div>
</div> </div>
)} )}
{label.labels && label.labels.length > 0 && ( {label.labels && label.labels.length > 0 && (
<div> <div>
<Text strong></Text> <Text strong></Text>
<div style={{ marginTop: 4 }}> <div style={{ marginTop: 4 }}>
{label.labels.map((lbl, i) => ( {label.labels.map((lbl, i) => (
<Tag key={i} color="geekblue">{lbl}</Tag> <Tag key={i} color="geekblue">{lbl}</Tag>
))} ))}
</div> </div>
</div> </div>
)} )}
</Space> </Space>
</Card> </Card>
))} ))}
</Space> </Space>
</Card> </Card>
{template.labelConfig && ( {template.labelConfig && (
<Card title="Label Studio XML 配置" size="small"> <Card title="Label Studio XML 配置" size="small">
<Paragraph> <Paragraph>
<pre style={{ <pre style={{
background: "#f5f5f5", background: "#f5f5f5",
padding: 12, padding: 12,
borderRadius: 4, borderRadius: 4,
overflow: "auto", overflow: "auto",
maxHeight: 300 maxHeight: 300
}}> }}>
{template.labelConfig} {template.labelConfig}
</pre> </pre>
</Paragraph> </Paragraph>
</Card> </Card>
)} )}
</Modal> </Modal>
); );
}; };
export default TemplateDetail; export default TemplateDetail;

View File

@@ -1,427 +1,427 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import { import {
Modal, Modal,
Form, Form,
Input, Input,
Select, Select,
Button, Button,
Space, Space,
message, message,
Divider, Divider,
Card, Card,
Checkbox, Checkbox,
} from "antd"; } from "antd";
import { PlusOutlined, MinusCircleOutlined } from "@ant-design/icons"; import { PlusOutlined, MinusCircleOutlined } from "@ant-design/icons";
import { import {
createAnnotationTemplateUsingPost, createAnnotationTemplateUsingPost,
updateAnnotationTemplateByIdUsingPut, updateAnnotationTemplateByIdUsingPut,
} from "../annotation.api"; } from "../annotation.api";
import type { AnnotationTemplate } from "../annotation.model"; import type { AnnotationTemplate } from "../annotation.model";
import TagSelector from "./components/TagSelector"; import TagSelector from "./components/TagSelector";
const { TextArea } = Input; const { TextArea } = Input;
const { Option } = Select; const { Option } = Select;
interface TemplateFormProps { interface TemplateFormProps {
visible: boolean; visible: boolean;
mode: "create" | "edit"; mode: "create" | "edit";
template?: AnnotationTemplate; template?: AnnotationTemplate;
onSuccess: () => void; onSuccess: () => void;
onCancel: () => void; onCancel: () => void;
} }
const TemplateForm: React.FC<TemplateFormProps> = ({ const TemplateForm: React.FC<TemplateFormProps> = ({
visible, visible,
mode, mode,
template, template,
onSuccess, onSuccess,
onCancel, onCancel,
}) => { }) => {
const [form] = Form.useForm(); const [form] = Form.useForm();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
useEffect(() => { useEffect(() => {
if (visible && template && mode === "edit") { if (visible && template && mode === "edit") {
form.setFieldsValue({ form.setFieldsValue({
name: template.name, name: template.name,
description: template.description, description: template.description,
dataType: template.dataType, dataType: template.dataType,
labelingType: template.labelingType, labelingType: template.labelingType,
style: template.style, style: template.style,
category: template.category, category: template.category,
labels: template.configuration.labels, labels: template.configuration.labels,
objects: template.configuration.objects, objects: template.configuration.objects,
}); });
} else if (visible && mode === "create") { } else if (visible && mode === "create") {
form.resetFields(); form.resetFields();
// Set default values // Set default values
form.setFieldsValue({ form.setFieldsValue({
style: "horizontal", style: "horizontal",
category: "custom", category: "custom",
labels: [], labels: [],
objects: [{ name: "image", type: "Image", value: "$image" }], objects: [{ name: "image", type: "Image", value: "$image" }],
}); });
} }
}, [visible, template, mode, form]); }, [visible, template, mode, form]);
const handleSubmit = async () => { const handleSubmit = async () => {
try { try {
const values = await form.validateFields(); const values = await form.validateFields();
setLoading(true); setLoading(true);
console.log("Form values:", values); console.log("Form values:", values);
const requestData = { const requestData = {
name: values.name, name: values.name,
description: values.description, description: values.description,
dataType: values.dataType, dataType: values.dataType,
labelingType: values.labelingType, labelingType: values.labelingType,
style: values.style, style: values.style,
category: values.category, category: values.category,
configuration: { configuration: {
labels: values.labels, labels: values.labels,
objects: values.objects, objects: values.objects,
}, },
}; };
console.log("Request data:", requestData); console.log("Request data:", requestData);
let response; let response;
if (mode === "create") { if (mode === "create") {
response = await createAnnotationTemplateUsingPost(requestData); response = await createAnnotationTemplateUsingPost(requestData);
} else { } else {
response = await updateAnnotationTemplateByIdUsingPut(template!.id, requestData); response = await updateAnnotationTemplateByIdUsingPut(template!.id, requestData);
} }
if (response.code === 200) { if (response.code === 200) {
message.success(`模板${mode === "create" ? "创建" : "更新"}成功`); message.success(`模板${mode === "create" ? "创建" : "更新"}成功`);
form.resetFields(); form.resetFields();
onSuccess(); onSuccess();
} else { } else {
message.error(response.message || `模板${mode === "create" ? "创建" : "更新"}失败`); message.error(response.message || `模板${mode === "create" ? "创建" : "更新"}失败`);
} }
} catch (error: any) { } catch (error: any) {
if (error.errorFields) { if (error.errorFields) {
message.error("请填写所有必填字段"); message.error("请填写所有必填字段");
} else { } else {
message.error(`模板${mode === "create" ? "创建" : "更新"}失败`); message.error(`模板${mode === "create" ? "创建" : "更新"}失败`);
console.error(error); console.error(error);
} }
} finally { } finally {
setLoading(false); setLoading(false);
} }
}; };
const needsOptions = (type: string) => { const needsOptions = (type: string) => {
return ["Choices", "RectangleLabels", "PolygonLabels", "Labels"].includes(type); return ["Choices", "RectangleLabels", "PolygonLabels", "Labels"].includes(type);
}; };
return ( return (
<Modal <Modal
title={mode === "create" ? "创建模板" : "编辑模板"} title={mode === "create" ? "创建模板" : "编辑模板"}
open={visible} open={visible}
onCancel={onCancel} onCancel={onCancel}
onOk={handleSubmit} onOk={handleSubmit}
confirmLoading={loading} confirmLoading={loading}
width={900} width={900}
destroyOnClose destroyOnClose
> >
<Form <Form
form={form} form={form}
layout="vertical" layout="vertical"
style={{ maxHeight: "70vh", overflowY: "auto", paddingRight: 8 }} style={{ maxHeight: "70vh", overflowY: "auto", paddingRight: 8 }}
> >
<Form.Item <Form.Item
label="模板名称" label="模板名称"
name="name" name="name"
rules={[{ required: true, message: "请输入模板名称" }]} rules={[{ required: true, message: "请输入模板名称" }]}
> >
<Input placeholder="例如:产品质量分类" maxLength={100} /> <Input placeholder="例如:产品质量分类" maxLength={100} />
</Form.Item> </Form.Item>
<Form.Item label="描述" name="description"> <Form.Item label="描述" name="description">
<TextArea <TextArea
placeholder="描述此模板的用途" placeholder="描述此模板的用途"
rows={2} rows={2}
maxLength={500} maxLength={500}
/> />
</Form.Item> </Form.Item>
<Space style={{ width: "100%" }} size="large"> <Space style={{ width: "100%" }} size="large">
<Form.Item <Form.Item
label="数据类型" label="数据类型"
name="dataType" name="dataType"
rules={[{ required: true, message: "请选择数据类型" }]} rules={[{ required: true, message: "请选择数据类型" }]}
style={{ width: 200 }} style={{ width: 200 }}
> >
<Select placeholder="选择数据类型"> <Select placeholder="选择数据类型">
<Option value="image"></Option> <Option value="image"></Option>
<Option value="text"></Option> <Option value="text"></Option>
<Option value="audio"></Option> <Option value="audio"></Option>
<Option value="video"></Option> <Option value="video"></Option>
</Select> </Select>
</Form.Item> </Form.Item>
<Form.Item <Form.Item
label="标注类型" label="标注类型"
name="labelingType" name="labelingType"
rules={[{ required: true, message: "请选择标注类型" }]} rules={[{ required: true, message: "请选择标注类型" }]}
style={{ width: 220 }} style={{ width: 220 }}
> >
<Select placeholder="选择标注类型"> <Select placeholder="选择标注类型">
<Option value="classification"></Option> <Option value="classification"></Option>
<Option value="object-detection"></Option> <Option value="object-detection"></Option>
<Option value="segmentation"></Option> <Option value="segmentation"></Option>
<Option value="ner"></Option> <Option value="ner"></Option>
<Option value="multi-stage"></Option> <Option value="multi-stage"></Option>
</Select> </Select>
</Form.Item> </Form.Item>
<Form.Item <Form.Item
label="样式" label="样式"
name="style" name="style"
style={{ width: 150 }} style={{ width: 150 }}
> >
<Select> <Select>
<Option value="horizontal"></Option> <Option value="horizontal"></Option>
<Option value="vertical"></Option> <Option value="vertical"></Option>
</Select> </Select>
</Form.Item> </Form.Item>
<Form.Item <Form.Item
label="分类" label="分类"
name="category" name="category"
style={{ width: 180 }} style={{ width: 180 }}
> >
<Select> <Select>
<Option value="computer-vision"></Option> <Option value="computer-vision"></Option>
<Option value="nlp"></Option> <Option value="nlp"></Option>
<Option value="audio"></Option> <Option value="audio"></Option>
<Option value="quality-control"></Option> <Option value="quality-control"></Option>
<Option value="custom"></Option> <Option value="custom"></Option>
</Select> </Select>
</Form.Item> </Form.Item>
</Space> </Space>
<Divider></Divider> <Divider></Divider>
<Form.List name="objects"> <Form.List name="objects">
{(fields, { add, remove }) => ( {(fields, { add, remove }) => (
<> <>
{fields.map((field) => ( {fields.map((field) => (
<Card key={field.key} size="small" style={{ marginBottom: 8 }}> <Card key={field.key} size="small" style={{ marginBottom: 8 }}>
<Space align="start" style={{ width: "100%" }}> <Space align="start" style={{ width: "100%" }}>
<Form.Item <Form.Item
{...field} {...field}
label="名称" label="名称"
name={[field.name, "name"]} name={[field.name, "name"]}
rules={[{ required: true, message: "必填" }]} rules={[{ required: true, message: "必填" }]}
style={{ marginBottom: 0, width: 150 }} style={{ marginBottom: 0, width: 150 }}
> >
<Input placeholder="例如:image" /> <Input placeholder="例如:image" />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
{...field} {...field}
label="类型" label="类型"
name={[field.name, "type"]} name={[field.name, "type"]}
rules={[{ required: true, message: "必填" }]} rules={[{ required: true, message: "必填" }]}
style={{ marginBottom: 0, width: 150 }} style={{ marginBottom: 0, width: 150 }}
> >
<TagSelector type="object" /> <TagSelector type="object" />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
{...field} {...field}
label="值" label="值"
name={[field.name, "value"]} name={[field.name, "value"]}
rules={[ rules={[
{ required: true, message: "必填" }, { required: true, message: "必填" },
{ pattern: /^\$/, message: "必须以 $ 开头" }, { pattern: /^\$/, message: "必须以 $ 开头" },
]} ]}
style={{ marginBottom: 0, width: 150 }} style={{ marginBottom: 0, width: 150 }}
> >
<Input placeholder="$image" /> <Input placeholder="$image" />
</Form.Item> </Form.Item>
{fields.length > 1 && ( {fields.length > 1 && (
<MinusCircleOutlined <MinusCircleOutlined
style={{ marginTop: 30, color: "red" }} style={{ marginTop: 30, color: "red" }}
onClick={() => remove(field.name)} onClick={() => remove(field.name)}
/> />
)} )}
</Space> </Space>
</Card> </Card>
))} ))}
<Button type="dashed" onClick={() => add()} block icon={<PlusOutlined />}> <Button type="dashed" onClick={() => add()} block icon={<PlusOutlined />}>
</Button> </Button>
</> </>
)} )}
</Form.List> </Form.List>
<Divider></Divider> <Divider></Divider>
<Form.List name="labels"> <Form.List name="labels">
{(fields, { add, remove }) => ( {(fields, { add, remove }) => (
<> <>
{fields.map((field) => ( {fields.map((field) => (
<Card <Card
key={field.key} key={field.key}
size="small" size="small"
style={{ marginBottom: 12 }} style={{ marginBottom: 12 }}
title={ title={
<Space> <Space>
<span> {fields.indexOf(field) + 1}</span> <span> {fields.indexOf(field) + 1}</span>
<Form.Item noStyle shouldUpdate> <Form.Item noStyle shouldUpdate>
{() => { {() => {
const controlType = form.getFieldValue(["labels", field.name, "type"]); const controlType = form.getFieldValue(["labels", field.name, "type"]);
const fromName = form.getFieldValue(["labels", field.name, "fromName"]); const fromName = form.getFieldValue(["labels", field.name, "fromName"]);
if (controlType || fromName) { if (controlType || fromName) {
return ( return (
<span style={{ fontSize: 12, fontWeight: 'normal', color: '#999' }}> <span style={{ fontSize: 12, fontWeight: 'normal', color: '#999' }}>
({fromName || '未命名'} - {controlType || '未设置类型'}) ({fromName || '未命名'} - {controlType || '未设置类型'})
</span> </span>
); );
} }
return null; return null;
}} }}
</Form.Item> </Form.Item>
</Space> </Space>
} }
extra={ extra={
<MinusCircleOutlined <MinusCircleOutlined
style={{ color: "red" }} style={{ color: "red" }}
onClick={() => remove(field.name)} onClick={() => remove(field.name)}
/> />
} }
> >
<Space direction="vertical" style={{ width: "100%" }} size="middle"> <Space direction="vertical" style={{ width: "100%" }} size="middle">
{/* Row 1: 控件名称, 标注目标对象, 控件类型 */} {/* Row 1: 控件名称, 标注目标对象, 控件类型 */}
<div style={{ display: 'grid', gridTemplateColumns: '180px 220px 1fr auto', gap: 12, alignItems: 'flex-end' }}> <div style={{ display: 'grid', gridTemplateColumns: '180px 220px 1fr auto', gap: 12, alignItems: 'flex-end' }}>
<Form.Item <Form.Item
{...field} {...field}
label="来源名称" label="来源名称"
name={[field.name, "fromName"]} name={[field.name, "fromName"]}
rules={[{ required: true, message: "必填" }]} rules={[{ required: true, message: "必填" }]}
style={{ marginBottom: 0 }} style={{ marginBottom: 0 }}
tooltip="此控件的唯一标识符" tooltip="此控件的唯一标识符"
> >
<Input placeholder="例如:choice" /> <Input placeholder="例如:choice" />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
{...field} {...field}
label="标注目标对象" label="标注目标对象"
name={[field.name, "toName"]} name={[field.name, "toName"]}
rules={[{ required: true, message: "必填" }]} rules={[{ required: true, message: "必填" }]}
style={{ marginBottom: 0 }} style={{ marginBottom: 0 }}
tooltip="选择此控件将标注哪个数据对象" tooltip="选择此控件将标注哪个数据对象"
dependencies={['objects']} dependencies={['objects']}
> >
<Select placeholder="选择数据对象"> <Select placeholder="选择数据对象">
{(form.getFieldValue("objects") || []).map((obj: any, idx: number) => ( {(form.getFieldValue("objects") || []).map((obj: any, idx: number) => (
<Option key={idx} value={obj?.name || ''}> <Option key={idx} value={obj?.name || ''}>
{obj?.name || `对象 ${idx + 1}`} ({obj?.type || '未知类型'}) {obj?.name || `对象 ${idx + 1}`} ({obj?.type || '未知类型'})
</Option> </Option>
))} ))}
</Select> </Select>
</Form.Item> </Form.Item>
<Form.Item <Form.Item
{...field} {...field}
label="控件类型" label="控件类型"
name={[field.name, "type"]} name={[field.name, "type"]}
rules={[{ required: true, message: "必填" }]} rules={[{ required: true, message: "必填" }]}
style={{ marginBottom: 0 }} style={{ marginBottom: 0 }}
> >
<TagSelector type="control" /> <TagSelector type="control" />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
{...field} {...field}
label=" " label=" "
name={[field.name, "required"]} name={[field.name, "required"]}
valuePropName="checked" valuePropName="checked"
style={{ marginBottom: 0 }} style={{ marginBottom: 0 }}
> >
<Checkbox></Checkbox> <Checkbox></Checkbox>
</Form.Item> </Form.Item>
</div> </div>
{/* Row 2: 取值范围定义(添加选项) - Conditionally rendered based on type */} {/* Row 2: 取值范围定义(添加选项) - Conditionally rendered based on type */}
<Form.Item <Form.Item
noStyle noStyle
shouldUpdate={(prevValues, currentValues) => { shouldUpdate={(prevValues, currentValues) => {
const prevType = prevValues.labels?.[field.name]?.type; const prevType = prevValues.labels?.[field.name]?.type;
const currType = currentValues.labels?.[field.name]?.type; const currType = currentValues.labels?.[field.name]?.type;
return prevType !== currType; return prevType !== currType;
}} }}
> >
{({ getFieldValue }) => { {({ getFieldValue }) => {
const controlType = getFieldValue(["labels", field.name, "type"]); const controlType = getFieldValue(["labels", field.name, "type"]);
const fieldName = controlType === "Choices" ? "options" : "labels"; const fieldName = controlType === "Choices" ? "options" : "labels";
if (needsOptions(controlType)) { if (needsOptions(controlType)) {
return ( return (
<Form.Item <Form.Item
{...field} {...field}
label={controlType === "Choices" ? "选项" : "标签"} label={controlType === "Choices" ? "选项" : "标签"}
name={[field.name, fieldName]} name={[field.name, fieldName]}
rules={[{ required: true, message: "至少需要一个选项" }]} rules={[{ required: true, message: "至少需要一个选项" }]}
style={{ marginBottom: 0 }} style={{ marginBottom: 0 }}
> >
<Select <Select
mode="tags" mode="tags"
open={false} open={false}
placeholder={ placeholder={
controlType === "Choices" controlType === "Choices"
? "输入选项内容,按回车添加。例如:是、否、不确定" ? "输入选项内容,按回车添加。例如:是、否、不确定"
: "输入标签名称,按回车添加。例如:人物、车辆、建筑物" : "输入标签名称,按回车添加。例如:人物、车辆、建筑物"
} }
style={{ width: "100%" }} style={{ width: "100%" }}
/> />
</Form.Item> </Form.Item>
); );
} }
return null; return null;
}} }}
</Form.Item> </Form.Item>
{/* Row 3: 描述 */} {/* Row 3: 描述 */}
<Form.Item <Form.Item
{...field} {...field}
label="描述" label="描述"
name={[field.name, "description"]} name={[field.name, "description"]}
style={{ marginBottom: 0 }} style={{ marginBottom: 0 }}
tooltip="向标注人员显示的帮助信息" tooltip="向标注人员显示的帮助信息"
> >
<Input placeholder="为标注人员提供此控件的使用说明" maxLength={200} /> <Input placeholder="为标注人员提供此控件的使用说明" maxLength={200} />
</Form.Item> </Form.Item>
</Space> </Space>
</Card> </Card>
))} ))}
<Button <Button
type="dashed" type="dashed"
onClick={() => onClick={() =>
add({ add({
fromName: "", fromName: "",
toName: "", toName: "",
type: "Choices", type: "Choices",
required: false, required: false,
}) })
} }
block block
icon={<PlusOutlined />} icon={<PlusOutlined />}
> >
</Button> </Button>
</> </>
)} )}
</Form.List> </Form.List>
</Form> </Form>
</Modal> </Modal>
); );
}; };
export default TemplateForm; export default TemplateForm;

View File

@@ -1,311 +1,311 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { import {
Button, Button,
Table, Table,
Space, Space,
Tag, Tag,
message, message,
Tooltip, Tooltip,
Popconfirm, Popconfirm,
Card, Card,
} from "antd"; } from "antd";
import { import {
PlusOutlined, PlusOutlined,
EditOutlined, EditOutlined,
DeleteOutlined, DeleteOutlined,
EyeOutlined, EyeOutlined,
} from "@ant-design/icons"; } from "@ant-design/icons";
import type { ColumnsType } from "antd/es/table"; import type { ColumnsType } from "antd/es/table";
import { import {
queryAnnotationTemplatesUsingGet, queryAnnotationTemplatesUsingGet,
deleteAnnotationTemplateByIdUsingDelete, deleteAnnotationTemplateByIdUsingDelete,
} from "../annotation.api"; } from "../annotation.api";
import type { AnnotationTemplate } from "../annotation.model"; import type { AnnotationTemplate } from "../annotation.model";
import TemplateForm from "./TemplateForm.tsx"; import TemplateForm from "./TemplateForm.tsx";
import TemplateDetail from "./TemplateDetail.tsx"; import TemplateDetail from "./TemplateDetail.tsx";
import {SearchControls} from "@/components/SearchControls.tsx"; import {SearchControls} from "@/components/SearchControls.tsx";
import useFetchData from "@/hooks/useFetchData.ts"; import useFetchData from "@/hooks/useFetchData.ts";
import { import {
AnnotationTypeMap, AnnotationTypeMap,
ClassificationMap, ClassificationMap,
DataTypeMap, DataTypeMap,
TemplateTypeMap TemplateTypeMap
} from "@/pages/DataAnnotation/annotation.const.tsx"; } from "@/pages/DataAnnotation/annotation.const.tsx";
const TemplateList: React.FC = () => { const TemplateList: React.FC = () => {
const filterOptions = [ const filterOptions = [
{ {
key: "category", key: "category",
label: "分类", label: "分类",
options: [...Object.values(ClassificationMap)], options: [...Object.values(ClassificationMap)],
}, },
{ {
key: "dataType", key: "dataType",
label: "数据类型", label: "数据类型",
options: [...Object.values(DataTypeMap)], options: [...Object.values(DataTypeMap)],
}, },
{ {
key: "labelingType", key: "labelingType",
label: "标注类型", label: "标注类型",
options: [...Object.values(AnnotationTypeMap)], options: [...Object.values(AnnotationTypeMap)],
}, },
{ {
key: "builtIn", key: "builtIn",
label: "模板类型", label: "模板类型",
options: [...Object.values(TemplateTypeMap)], options: [...Object.values(TemplateTypeMap)],
}, },
]; ];
// Modals // Modals
const [isFormVisible, setIsFormVisible] = useState(false); const [isFormVisible, setIsFormVisible] = useState(false);
const [isDetailVisible, setIsDetailVisible] = useState(false); const [isDetailVisible, setIsDetailVisible] = useState(false);
const [selectedTemplate, setSelectedTemplate] = useState<AnnotationTemplate | undefined>(); const [selectedTemplate, setSelectedTemplate] = useState<AnnotationTemplate | undefined>();
const [formMode, setFormMode] = useState<"create" | "edit">("create"); const [formMode, setFormMode] = useState<"create" | "edit">("create");
const { const {
loading, loading,
tableData, tableData,
pagination, pagination,
searchParams, searchParams,
setSearchParams, setSearchParams,
fetchData, fetchData,
handleFiltersChange, handleFiltersChange,
handleKeywordChange, handleKeywordChange,
} = useFetchData(queryAnnotationTemplatesUsingGet, undefined, undefined, undefined, undefined, 0); } = useFetchData(queryAnnotationTemplatesUsingGet, undefined, undefined, undefined, undefined, 0);
const handleCreate = () => { const handleCreate = () => {
setFormMode("create"); setFormMode("create");
setSelectedTemplate(undefined); setSelectedTemplate(undefined);
setIsFormVisible(true); setIsFormVisible(true);
}; };
const handleEdit = (template: AnnotationTemplate) => { const handleEdit = (template: AnnotationTemplate) => {
setFormMode("edit"); setFormMode("edit");
setSelectedTemplate(template); setSelectedTemplate(template);
setIsFormVisible(true); setIsFormVisible(true);
}; };
const handleView = (template: AnnotationTemplate) => { const handleView = (template: AnnotationTemplate) => {
setSelectedTemplate(template); setSelectedTemplate(template);
setIsDetailVisible(true); setIsDetailVisible(true);
}; };
const handleDelete = async (templateId: string) => { const handleDelete = async (templateId: string) => {
try { try {
const response = await deleteAnnotationTemplateByIdUsingDelete(templateId); const response = await deleteAnnotationTemplateByIdUsingDelete(templateId);
if (response.code === 200) { if (response.code === 200) {
message.success("模板删除成功"); message.success("模板删除成功");
fetchData(); fetchData();
} else { } else {
message.error(response.message || "删除模板失败"); message.error(response.message || "删除模板失败");
} }
} catch (error) { } catch (error) {
message.error("删除模板失败"); message.error("删除模板失败");
console.error(error); console.error(error);
} }
}; };
const handleFormSuccess = () => { const handleFormSuccess = () => {
setIsFormVisible(false); setIsFormVisible(false);
fetchData(); fetchData();
}; };
const getCategoryColor = (category: string) => { const getCategoryColor = (category: string) => {
const colors: Record<string, string> = { const colors: Record<string, string> = {
"computer-vision": "blue", "computer-vision": "blue",
"nlp": "green", "nlp": "green",
"audio": "purple", "audio": "purple",
"quality-control": "orange", "quality-control": "orange",
"custom": "default", "custom": "default",
}; };
return colors[category] || "default"; return colors[category] || "default";
}; };
const columns: ColumnsType<AnnotationTemplate> = [ const columns: ColumnsType<AnnotationTemplate> = [
{ {
title: "模板名称", title: "模板名称",
dataIndex: "name", dataIndex: "name",
key: "name", key: "name",
width: 200, width: 200,
ellipsis: true, ellipsis: true,
onFilter: (value, record) => onFilter: (value, record) =>
record.name.toLowerCase().includes(value.toString().toLowerCase()) || record.name.toLowerCase().includes(value.toString().toLowerCase()) ||
(record.description?.toLowerCase().includes(value.toString().toLowerCase()) ?? false), (record.description?.toLowerCase().includes(value.toString().toLowerCase()) ?? false),
}, },
{ {
title: "描述", title: "描述",
dataIndex: "description", dataIndex: "description",
key: "description", key: "description",
ellipsis: { ellipsis: {
showTitle: false, showTitle: false,
}, },
render: (description: string) => ( render: (description: string) => (
<Tooltip title={description}> <Tooltip title={description}>
<div <div
style={{ style={{
display: '-webkit-box', display: '-webkit-box',
WebkitLineClamp: 2, WebkitLineClamp: 2,
WebkitBoxOrient: 'vertical', WebkitBoxOrient: 'vertical',
overflow: 'hidden', overflow: 'hidden',
textOverflow: 'ellipsis', textOverflow: 'ellipsis',
whiteSpace: 'normal', whiteSpace: 'normal',
lineHeight: '1.5em', lineHeight: '1.5em',
maxHeight: '3em', maxHeight: '3em',
}} }}
> >
{description} {description}
</div> </div>
</Tooltip> </Tooltip>
), ),
}, },
{ {
title: "数据类型", title: "数据类型",
dataIndex: "dataType", dataIndex: "dataType",
key: "dataType", key: "dataType",
width: 120, width: 120,
render: (dataType: string) => ( render: (dataType: string) => (
<Tag color="cyan">{dataType}</Tag> <Tag color="cyan">{dataType}</Tag>
), ),
}, },
{ {
title: "标注类型", title: "标注类型",
dataIndex: "labelingType", dataIndex: "labelingType",
key: "labelingType", key: "labelingType",
width: 150, width: 150,
render: (labelingType: string) => ( render: (labelingType: string) => (
<Tag color="geekblue">{labelingType}</Tag> <Tag color="geekblue">{labelingType}</Tag>
), ),
}, },
{ {
title: "分类", title: "分类",
dataIndex: "category", dataIndex: "category",
key: "category", key: "category",
width: 150, width: 150,
render: (category: string) => ( render: (category: string) => (
<Tag color={getCategoryColor(category)}>{category}</Tag> <Tag color={getCategoryColor(category)}>{category}</Tag>
), ),
}, },
{ {
title: "类型", title: "类型",
dataIndex: "builtIn", dataIndex: "builtIn",
key: "builtIn", key: "builtIn",
width: 100, width: 100,
render: (builtIn: boolean) => ( render: (builtIn: boolean) => (
<Tag color={builtIn ? "gold" : "default"}> <Tag color={builtIn ? "gold" : "default"}>
{builtIn ? "系统内置" : "自定义"} {builtIn ? "系统内置" : "自定义"}
</Tag> </Tag>
), ),
}, },
{ {
title: "版本", title: "版本",
dataIndex: "version", dataIndex: "version",
key: "version", key: "version",
width: 80, width: 80,
}, },
{ {
title: "创建时间", title: "创建时间",
dataIndex: "createdAt", dataIndex: "createdAt",
key: "createdAt", key: "createdAt",
width: 180, width: 180,
render: (date: string) => new Date(date).toLocaleString(), render: (date: string) => new Date(date).toLocaleString(),
}, },
{ {
title: "操作", title: "操作",
key: "action", key: "action",
width: 200, width: 200,
fixed: "right", fixed: "right",
render: (_, record) => ( render: (_, record) => (
<Space size="small"> <Space size="small">
<Tooltip title="查看详情"> <Tooltip title="查看详情">
<Button <Button
type="link" type="link"
icon={<EyeOutlined />} icon={<EyeOutlined />}
onClick={() => handleView(record)} onClick={() => handleView(record)}
/> />
</Tooltip> </Tooltip>
{!record.builtIn && ( {!record.builtIn && (
<> <>
<Tooltip title="编辑"> <Tooltip title="编辑">
<Button <Button
type="link" type="link"
icon={<EditOutlined />} icon={<EditOutlined />}
onClick={() => handleEdit(record)} onClick={() => handleEdit(record)}
/> />
</Tooltip> </Tooltip>
<Popconfirm <Popconfirm
title="确定要删除这个模板吗?" title="确定要删除这个模板吗?"
onConfirm={() => handleDelete(record.id)} onConfirm={() => handleDelete(record.id)}
okText="确定" okText="确定"
cancelText="取消" cancelText="取消"
> >
<Tooltip title="删除"> <Tooltip title="删除">
<Button <Button
type="link" type="link"
danger danger
icon={<DeleteOutlined />} icon={<DeleteOutlined />}
/> />
</Tooltip> </Tooltip>
</Popconfirm> </Popconfirm>
</> </>
)} )}
</Space> </Space>
), ),
}, },
]; ];
return ( return (
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
{/* Search, Filters and Buttons in one row */} {/* Search, Filters and Buttons in one row */}
<div className="flex items-center justify-between gap-2"> <div className="flex items-center justify-between gap-2">
{/* Left side: Search and Filters */} {/* Left side: Search and Filters */}
<div className="flex items-center gap-2 flex-wrap"> <div className="flex items-center gap-2 flex-wrap">
<SearchControls <SearchControls
searchTerm={searchParams.keyword} searchTerm={searchParams.keyword}
onSearchChange={handleKeywordChange} onSearchChange={handleKeywordChange}
searchPlaceholder="搜索任务名称、描述" searchPlaceholder="搜索任务名称、描述"
filters={filterOptions} filters={filterOptions}
onFiltersChange={handleFiltersChange} onFiltersChange={handleFiltersChange}
showViewToggle={true} showViewToggle={true}
onReload={fetchData} onReload={fetchData}
onClearFilters={() => setSearchParams({ ...searchParams, filter: {} })} onClearFilters={() => setSearchParams({ ...searchParams, filter: {} })}
/> />
</div> </div>
{/* Right side: Create button */} {/* Right side: Create button */}
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Button type="primary" icon={<PlusOutlined />} onClick={handleCreate}> <Button type="primary" icon={<PlusOutlined />} onClick={handleCreate}>
</Button> </Button>
</div> </div>
</div> </div>
<Card> <Card>
<Table <Table
columns={columns} columns={columns}
dataSource={tableData} dataSource={tableData}
rowKey="id" rowKey="id"
loading={loading} loading={loading}
pagination={pagination} pagination={pagination}
scroll={{ x: 1400, y: "calc(100vh - 24rem)" }} scroll={{ x: 1400, y: "calc(100vh - 24rem)" }}
/> />
</Card> </Card>
<TemplateForm <TemplateForm
visible={isFormVisible} visible={isFormVisible}
mode={formMode} mode={formMode}
template={selectedTemplate} template={selectedTemplate}
onSuccess={handleFormSuccess} onSuccess={handleFormSuccess}
onCancel={() => setIsFormVisible(false)} onCancel={() => setIsFormVisible(false)}
/> />
<TemplateDetail <TemplateDetail
visible={isDetailVisible} visible={isDetailVisible}
template={selectedTemplate} template={selectedTemplate}
onClose={() => setIsDetailVisible(false)} onClose={() => setIsDetailVisible(false)}
/> />
</div> </div>
); );
}; };
export default TemplateList; export default TemplateList;
export { TemplateList }; export { TemplateList };

View File

@@ -1,161 +1,161 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { import {
Card, Card,
Button, Button,
Space, Space,
Row, Row,
Col, Col,
Drawer, Drawer,
Typography, Typography,
message, message,
} from "antd"; } from "antd";
import { import {
PlusOutlined, PlusOutlined,
EyeOutlined, EyeOutlined,
CodeOutlined, CodeOutlined,
AppstoreOutlined, AppstoreOutlined,
} from "@ant-design/icons"; } from "@ant-design/icons";
import { TagBrowser } from "./components"; import { TagBrowser } from "./components";
const { Paragraph } = Typography; const { Paragraph } = Typography;
interface VisualTemplateBuilderProps { interface VisualTemplateBuilderProps {
onSave?: (templateCode: string) => void; onSave?: (templateCode: string) => void;
} }
/** /**
* Visual Template Builder * Visual Template Builder
* Provides a drag-and-drop interface for building Label Studio templates * Provides a drag-and-drop interface for building Label Studio templates
*/ */
const VisualTemplateBuilder: React.FC<VisualTemplateBuilderProps> = ({ const VisualTemplateBuilder: React.FC<VisualTemplateBuilderProps> = ({
onSave, onSave,
}) => { }) => {
const [drawerVisible, setDrawerVisible] = useState(false); const [drawerVisible, setDrawerVisible] = useState(false);
const [previewVisible, setPreviewVisible] = useState(false); const [previewVisible, setPreviewVisible] = useState(false);
const [selectedTags, setSelectedTags] = useState< const [selectedTags, setSelectedTags] = useState<
Array<{ name: string; category: "object" | "control" }> Array<{ name: string; category: "object" | "control" }>
>([]); >([]);
const handleTagSelect = (tagName: string, category: "object" | "control") => { const handleTagSelect = (tagName: string, category: "object" | "control") => {
message.info(`选择了 ${category === "object" ? "对象" : "控件"}: ${tagName}`); message.info(`选择了 ${category === "object" ? "对象" : "控件"}: ${tagName}`);
setSelectedTags([...selectedTags, { name: tagName, category }]); setSelectedTags([...selectedTags, { name: tagName, category }]);
setDrawerVisible(false); setDrawerVisible(false);
}; };
const handleSave = () => { const handleSave = () => {
// TODO: Generate template XML from selectedTags // TODO: Generate template XML from selectedTags
message.success("模板保存成功"); message.success("模板保存成功");
onSave?.("<View><!-- Generated template --></View>"); onSave?.("<View><!-- Generated template --></View>");
}; };
return ( return (
<div style={{ padding: "24px" }}> <div style={{ padding: "24px" }}>
<Row gutter={16}> <Row gutter={16}>
<Col span={24}> <Col span={24}>
<Card <Card
title="可视化模板构建器" title="可视化模板构建器"
extra={ extra={
<Space> <Space>
<Button <Button
icon={<AppstoreOutlined />} icon={<AppstoreOutlined />}
onClick={() => setDrawerVisible(true)} onClick={() => setDrawerVisible(true)}
> >
</Button> </Button>
<Button <Button
icon={<CodeOutlined />} icon={<CodeOutlined />}
onClick={() => setPreviewVisible(true)} onClick={() => setPreviewVisible(true)}
> >
</Button> </Button>
<Button <Button
type="primary" type="primary"
icon={<EyeOutlined />} icon={<EyeOutlined />}
onClick={handleSave} onClick={handleSave}
> >
</Button> </Button>
</Space> </Space>
} }
> >
<div <div
style={{ style={{
minHeight: "400px", minHeight: "400px",
border: "2px dashed #d9d9d9", border: "2px dashed #d9d9d9",
borderRadius: "8px", borderRadius: "8px",
padding: "24px", padding: "24px",
textAlign: "center", textAlign: "center",
}} }}
> >
{selectedTags.length === 0 ? ( {selectedTags.length === 0 ? (
<div> <div>
<Paragraph type="secondary"> <Paragraph type="secondary">
"浏览标签" "浏览标签"
</Paragraph> </Paragraph>
<Button <Button
type="dashed" type="dashed"
icon={<PlusOutlined />} icon={<PlusOutlined />}
onClick={() => setDrawerVisible(true)} onClick={() => setDrawerVisible(true)}
> >
</Button> </Button>
</div> </div>
) : ( ) : (
<Space direction="vertical" size="large"> <Space direction="vertical" size="large">
{selectedTags.map((tag, index) => ( {selectedTags.map((tag, index) => (
<Card key={index} size="small"> <Card key={index} size="small">
<div> <div>
{tag.category === "object" ? "对象" : "控件"}: {tag.name} {tag.category === "object" ? "对象" : "控件"}: {tag.name}
</div> </div>
</Card> </Card>
))} ))}
</Space> </Space>
)} )}
</div> </div>
</Card> </Card>
</Col> </Col>
</Row> </Row>
<Drawer <Drawer
title="标签浏览器" title="标签浏览器"
placement="right" placement="right"
width={800} width={800}
open={drawerVisible} open={drawerVisible}
onClose={() => setDrawerVisible(false)} onClose={() => setDrawerVisible(false)}
> >
<TagBrowser onTagSelect={handleTagSelect} /> <TagBrowser onTagSelect={handleTagSelect} />
</Drawer> </Drawer>
<Drawer <Drawer
title="模板代码预览" title="模板代码预览"
placement="right" placement="right"
width={600} width={600}
open={previewVisible} open={previewVisible}
onClose={() => setPreviewVisible(false)} onClose={() => setPreviewVisible(false)}
> >
<pre <pre
style={{ style={{
background: "#f5f5f5", background: "#f5f5f5",
padding: "16px", padding: "16px",
borderRadius: "4px", borderRadius: "4px",
overflow: "auto", overflow: "auto",
}} }}
> >
<code> <code>
{`<View> {`<View>
<!-- 根据选择的标签生成的模板代码 --> <!-- 根据选择的标签生成的模板代码 -->
${selectedTags ${selectedTags
.map( .map(
(tag) => (tag) =>
`<${tag.name}${tag.category === "object" ? ' name="obj" value="$data"' : ' name="ctrl" toName="obj"'} />` `<${tag.name}${tag.category === "object" ? ' name="obj" value="$data"' : ' name="ctrl" toName="obj"'} />`
) )
.join("\n ")} .join("\n ")}
</View>`} </View>`}
</code> </code>
</pre> </pre>
</Drawer> </Drawer>
</div> </div>
); );
}; };
export default VisualTemplateBuilder; export default VisualTemplateBuilder;

View File

@@ -1,260 +1,260 @@
import React from "react"; import React from "react";
import { Card, Tabs, List, Tag, Typography, Space, Empty, Spin } from "antd"; import { Card, Tabs, List, Tag, Typography, Space, Empty, Spin } from "antd";
import { import {
AppstoreOutlined, AppstoreOutlined,
ControlOutlined, ControlOutlined,
InfoCircleOutlined, InfoCircleOutlined,
} from "@ant-design/icons"; } from "@ant-design/icons";
import { useTagConfig } from "../../../../hooks/useTagConfig"; import { useTagConfig } from "../../../../hooks/useTagConfig";
import { import {
getControlDisplayName, getControlDisplayName,
getObjectDisplayName, getObjectDisplayName,
getControlGroups, getControlGroups,
} from "../../annotation.tagconfig"; } from "../../annotation.tagconfig";
import type { TagOption } from "../../annotation.tagconfig"; import type { TagOption } from "../../annotation.tagconfig";
const { Title, Paragraph, Text } = Typography; const { Title, Paragraph, Text } = Typography;
interface TagBrowserProps { interface TagBrowserProps {
onTagSelect?: (tagName: string, category: "object" | "control") => void; onTagSelect?: (tagName: string, category: "object" | "control") => void;
} }
/** /**
* Tag Browser Component * Tag Browser Component
* Displays all available Label Studio tags in a browsable interface * Displays all available Label Studio tags in a browsable interface
*/ */
const TagBrowser: React.FC<TagBrowserProps> = ({ onTagSelect }) => { const TagBrowser: React.FC<TagBrowserProps> = ({ onTagSelect }) => {
const { config, objectOptions, controlOptions, loading, error } = const { config, objectOptions, controlOptions, loading, error } =
useTagConfig(); useTagConfig();
if (loading) { if (loading) {
return ( return (
<Card> <Card>
<div style={{ textAlign: "center", padding: "40px" }}> <div style={{ textAlign: "center", padding: "40px" }}>
<Spin size="large" /> <Spin size="large" />
<div style={{ marginTop: 16 }}>...</div> <div style={{ marginTop: 16 }}>...</div>
</div> </div>
</Card> </Card>
); );
} }
if (error) { if (error) {
return ( return (
<Card> <Card>
<Empty <Empty
description={ description={
<div> <div>
<div>{error}</div> <div>{error}</div>
<Text type="secondary"></Text> <Text type="secondary"></Text>
</div> </div>
} }
/> />
</Card> </Card>
); );
} }
const renderObjectList = () => ( const renderObjectList = () => (
<List <List
grid={{ gutter: 16, xs: 1, sm: 2, md: 2, lg: 3, xl: 3, xxl: 4 }} grid={{ gutter: 16, xs: 1, sm: 2, md: 2, lg: 3, xl: 3, xxl: 4 }}
dataSource={objectOptions} dataSource={objectOptions}
renderItem={(item: TagOption) => { renderItem={(item: TagOption) => {
const objConfig = config?.objects[item.value]; const objConfig = config?.objects[item.value];
return ( return (
<List.Item> <List.Item>
<Card <Card
hoverable hoverable
size="small" size="small"
onClick={() => onTagSelect?.(item.value, "object")} onClick={() => onTagSelect?.(item.value, "object")}
style={{ height: "100%" }} style={{ height: "100%" }}
> >
<Space direction="vertical" size="small" style={{ width: "100%" }}> <Space direction="vertical" size="small" style={{ width: "100%" }}>
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}> <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
<Text strong>{getObjectDisplayName(item.value)}</Text> <Text strong>{getObjectDisplayName(item.value)}</Text>
<Tag color="blue">&lt;{item.value}&gt;</Tag> <Tag color="blue">&lt;{item.value}&gt;</Tag>
</div> </div>
<Text type="secondary" style={{ fontSize: 12 }}> <Text type="secondary" style={{ fontSize: 12 }}>
{item.description} {item.description}
</Text> </Text>
{objConfig && ( {objConfig && (
<div style={{ marginTop: 8 }}> <div style={{ marginTop: 8 }}>
<Text style={{ fontSize: 11, color: "#8c8c8c" }}> <Text style={{ fontSize: 11, color: "#8c8c8c" }}>
:{" "} :{" "}
{objConfig.required_attrs.join(", ") || "无"} {objConfig.required_attrs.join(", ") || "无"}
</Text> </Text>
</div> </div>
)} )}
</Space> </Space>
</Card> </Card>
</List.Item> </List.Item>
); );
}} }}
/> />
); );
const renderControlsByGroup = () => { const renderControlsByGroup = () => {
const groups = getControlGroups(); const groups = getControlGroups();
return ( return (
<Tabs <Tabs
defaultActiveKey="classification" defaultActiveKey="classification"
items={Object.entries(groups).map(([groupKey, groupConfig]) => { items={Object.entries(groups).map(([groupKey, groupConfig]) => {
const groupControls = controlOptions.filter((opt: TagOption) => const groupControls = controlOptions.filter((opt: TagOption) =>
groupConfig.controls.includes(opt.value) groupConfig.controls.includes(opt.value)
); );
return { return {
key: groupKey, key: groupKey,
label: groupConfig.label, label: groupConfig.label,
children: ( children: (
<List <List
grid={{ gutter: 16, xs: 1, sm: 2, md: 2, lg: 3, xl: 3, xxl: 4 }} grid={{ gutter: 16, xs: 1, sm: 2, md: 2, lg: 3, xl: 3, xxl: 4 }}
dataSource={groupControls} dataSource={groupControls}
locale={{ emptyText: "此分组暂无控件" }} locale={{ emptyText: "此分组暂无控件" }}
renderItem={(item: TagOption) => { renderItem={(item: TagOption) => {
const ctrlConfig = config?.controls[item.value]; const ctrlConfig = config?.controls[item.value];
return ( return (
<List.Item> <List.Item>
<Card <Card
hoverable hoverable
size="small" size="small"
onClick={() => onTagSelect?.(item.value, "control")} onClick={() => onTagSelect?.(item.value, "control")}
style={{ height: "100%" }} style={{ height: "100%" }}
> >
<Space direction="vertical" size="small" style={{ width: "100%" }}> <Space direction="vertical" size="small" style={{ width: "100%" }}>
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}> <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
<Text strong> <Text strong>
{getControlDisplayName(item.value)} {getControlDisplayName(item.value)}
</Text> </Text>
<Tag color="green">&lt;{item.value}&gt;</Tag> <Tag color="green">&lt;{item.value}&gt;</Tag>
</div> </div>
<Text type="secondary" style={{ fontSize: 12 }}> <Text type="secondary" style={{ fontSize: 12 }}>
{item.description} {item.description}
</Text> </Text>
{ctrlConfig && ( {ctrlConfig && (
<Space <Space
size={4} size={4}
wrap wrap
style={{ marginTop: 8 }} style={{ marginTop: 8 }}
> >
{ctrlConfig.requires_children && ( {ctrlConfig.requires_children && (
<Tag <Tag
color="orange" color="orange"
style={{ fontSize: 10, margin: 0 }} style={{ fontSize: 10, margin: 0 }}
> >
&lt;{ctrlConfig.child_tag}&gt; &lt;{ctrlConfig.child_tag}&gt;
</Tag> </Tag>
)} )}
{ctrlConfig.required_attrs.includes("toName") && ( {ctrlConfig.required_attrs.includes("toName") && (
<Tag <Tag
color="purple" color="purple"
style={{ fontSize: 10, margin: 0 }} style={{ fontSize: 10, margin: 0 }}
> >
</Tag> </Tag>
)} )}
</Space> </Space>
)} )}
</Space> </Space>
</Card> </Card>
</List.Item> </List.Item>
); );
}} }}
/> />
), ),
}; };
})} })}
/> />
); );
}; };
return ( return (
<Card> <Card>
<Tabs <Tabs
defaultActiveKey="controls" defaultActiveKey="controls"
items={[ items={[
{ {
key: "controls", key: "controls",
label: ( label: (
<span> <span>
<ControlOutlined /> <ControlOutlined />
({controlOptions.length}) ({controlOptions.length})
</span> </span>
), ),
children: renderControlsByGroup(), children: renderControlsByGroup(),
}, },
{ {
key: "objects", key: "objects",
label: ( label: (
<span> <span>
<AppstoreOutlined /> <AppstoreOutlined />
({objectOptions.length}) ({objectOptions.length})
</span> </span>
), ),
children: renderObjectList(), children: renderObjectList(),
}, },
{ {
key: "help", key: "help",
label: ( label: (
<span> <span>
<InfoCircleOutlined /> <InfoCircleOutlined />
使 使
</span> </span>
), ),
children: ( children: (
<div style={{ padding: "16px" }}> <div style={{ padding: "16px" }}>
<Title level={4}>Label Studio </Title> <Title level={4}>Label Studio </Title>
<Paragraph> <Paragraph>
</Paragraph> </Paragraph>
<ul> <ul>
<li> <li>
<Text strong></Text> <Text strong></Text>
</li> </li>
<li> <li>
<Text strong></Text> <Text strong></Text>
</li> </li>
</ul> </ul>
<Title level={5} style={{ marginTop: 24 }}> <Title level={5} style={{ marginTop: 24 }}>
</Title> </Title>
<Paragraph> <Paragraph>
<pre style={{ background: "#f5f5f5", padding: 12, borderRadius: 4 }}> <pre style={{ background: "#f5f5f5", padding: 12, borderRadius: 4 }}>
{`<View> {`<View>
<!-- 数据对象 --> <!-- 数据对象 -->
<Image name="image" value="$image" /> <Image name="image" value="$image" />
<!-- 控件 --> <!-- 控件 -->
<RectangleLabels name="label" toName="image"> <RectangleLabels name="label" toName="image">
<Label value="人物" /> <Label value="人物" />
<Label value="车辆" /> <Label value="车辆" />
</RectangleLabels> </RectangleLabels>
</View>`} </View>`}
</pre> </pre>
</Paragraph> </Paragraph>
<Title level={5} style={{ marginTop: 24 }}> <Title level={5} style={{ marginTop: 24 }}>
</Title> </Title>
<ul> <ul>
<li> <li>
<Text code>name</Text> <Text code>name</Text>
</li> </li>
<li> <li>
<Text code>toName</Text> name <Text code>toName</Text> name
</li> </li>
<li> <li>
<Text code>value</Text> $ $image, $text <Text code>value</Text> $ $image, $text
</li> </li>
<li> <li>
<Text code>required</Text> <Text code>required</Text>
</li> </li>
</ul> </ul>
</div> </div>
), ),
}, },
]} ]}
/> />
</Card> </Card>
); );
}; };
export default TagBrowser; export default TagBrowser;

View File

@@ -1,301 +1,301 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import { Select, Tooltip, Spin, Collapse, Tag, Space } from "antd"; import { Select, Tooltip, Spin, Collapse, Tag, Space } from "antd";
import { InfoCircleOutlined } from "@ant-design/icons"; import { InfoCircleOutlined } from "@ant-design/icons";
import { getTagConfigUsingGet } from "../../annotation.api"; import { getTagConfigUsingGet } from "../../annotation.api";
import type { import type {
LabelStudioTagConfig, LabelStudioTagConfig,
TagOption, TagOption,
} from "../../annotation.tagconfig"; } from "../../annotation.tagconfig";
import { import {
parseTagConfig, parseTagConfig,
getControlDisplayName, getControlDisplayName,
getObjectDisplayName, getObjectDisplayName,
getControlGroups, getControlGroups,
} from "../../annotation.tagconfig"; } from "../../annotation.tagconfig";
const { Option, OptGroup } = Select; const { Option, OptGroup } = Select;
interface TagSelectorProps { interface TagSelectorProps {
value?: string; value?: string;
onChange?: (value: string) => void; onChange?: (value: string) => void;
type: "object" | "control"; type: "object" | "control";
placeholder?: string; placeholder?: string;
style?: React.CSSProperties; style?: React.CSSProperties;
disabled?: boolean; disabled?: boolean;
} }
/** /**
* Tag Selector Component * Tag Selector Component
* Dynamically fetches and displays available Label Studio tags from backend config * Dynamically fetches and displays available Label Studio tags from backend config
*/ */
const TagSelector: React.FC<TagSelectorProps> = ({ const TagSelector: React.FC<TagSelectorProps> = ({
value, value,
onChange, onChange,
type, type,
placeholder, placeholder,
style, style,
disabled, disabled,
}) => { }) => {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [tagOptions, setTagOptions] = useState<TagOption[]>([]); const [tagOptions, setTagOptions] = useState<TagOption[]>([]);
useEffect(() => { useEffect(() => {
fetchTagConfig(); fetchTagConfig();
}, []); }, []);
const fetchTagConfig = async () => { const fetchTagConfig = async () => {
setLoading(true); setLoading(true);
setError(null); setError(null);
try { try {
const response = await getTagConfigUsingGet(); const response = await getTagConfigUsingGet();
if (response.code === 200 && response.data) { if (response.code === 200 && response.data) {
const config: LabelStudioTagConfig = response.data; const config: LabelStudioTagConfig = response.data;
const { objectOptions, controlOptions } = parseTagConfig(config); const { objectOptions, controlOptions } = parseTagConfig(config);
if (type === "object") { if (type === "object") {
setTagOptions(objectOptions); setTagOptions(objectOptions);
} else { } else {
setTagOptions(controlOptions); setTagOptions(controlOptions);
} }
} else { } else {
setError(response.message || "获取标签配置失败"); setError(response.message || "获取标签配置失败");
} }
} catch (err: any) { } catch (err: any) {
console.error("Failed to fetch tag config:", err); console.error("Failed to fetch tag config:", err);
setError("加载标签配置时出错"); setError("加载标签配置时出错");
} finally { } finally {
setLoading(false); setLoading(false);
} }
}; };
if (loading) { if (loading) {
return ( return (
<Select <Select
placeholder="加载中..." placeholder="加载中..."
style={style} style={style}
disabled disabled
suffixIcon={<Spin size="small" />} suffixIcon={<Spin size="small" />}
/> />
); );
} }
if (error) { if (error) {
return ( return (
<Tooltip title={error}> <Tooltip title={error}>
<Select <Select
placeholder="加载失败,点击重试" placeholder="加载失败,点击重试"
style={style} style={style}
disabled={disabled} disabled={disabled}
status="error" status="error"
onClick={() => fetchTagConfig()} onClick={() => fetchTagConfig()}
/> />
</Tooltip> </Tooltip>
); );
} }
// Group controls by usage pattern // Group controls by usage pattern
if (type === "control") { if (type === "control") {
const groups = getControlGroups(); const groups = getControlGroups();
const groupedOptions: Record<string, TagOption[]> = {}; const groupedOptions: Record<string, TagOption[]> = {};
const ungroupedOptions: TagOption[] = []; const ungroupedOptions: TagOption[] = [];
// Group the controls // Group the controls
Object.entries(groups).forEach(([groupKey, groupConfig]) => { Object.entries(groups).forEach(([groupKey, groupConfig]) => {
groupedOptions[groupKey] = tagOptions.filter((opt) => groupedOptions[groupKey] = tagOptions.filter((opt) =>
groupConfig.controls.includes(opt.value) groupConfig.controls.includes(opt.value)
); );
}); });
// Find ungrouped controls // Find ungrouped controls
const allGroupedControls = new Set( const allGroupedControls = new Set(
Object.values(groups).flatMap((g) => g.controls) Object.values(groups).flatMap((g) => g.controls)
); );
tagOptions.forEach((opt) => { tagOptions.forEach((opt) => {
if (!allGroupedControls.has(opt.value)) { if (!allGroupedControls.has(opt.value)) {
ungroupedOptions.push(opt); ungroupedOptions.push(opt);
} }
}); });
return ( return (
<Select <Select
value={value} value={value}
onChange={onChange} onChange={onChange}
placeholder={placeholder || "选择控件类型"} placeholder={placeholder || "选择控件类型"}
style={style} style={style}
disabled={disabled} disabled={disabled}
showSearch showSearch
optionFilterProp="label" optionFilterProp="label"
> >
{Object.entries(groups).map(([groupKey, groupConfig]) => { {Object.entries(groups).map(([groupKey, groupConfig]) => {
const options = groupedOptions[groupKey]; const options = groupedOptions[groupKey];
if (options.length === 0) return null; if (options.length === 0) return null;
return ( return (
<OptGroup key={groupKey} label={groupConfig.label}> <OptGroup key={groupKey} label={groupConfig.label}>
{options.map((opt) => ( {options.map((opt) => (
<Option key={opt.value} value={opt.value} label={opt.label}> <Option key={opt.value} value={opt.value} label={opt.label}>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<span>{getControlDisplayName(opt.value)}</span> <span>{getControlDisplayName(opt.value)}</span>
<Tooltip title={opt.description}> <Tooltip title={opt.description}>
<InfoCircleOutlined <InfoCircleOutlined
style={{ color: "#8c8c8c", fontSize: 12 }} style={{ color: "#8c8c8c", fontSize: 12 }}
/> />
</Tooltip> </Tooltip>
</div> </div>
</Option> </Option>
))} ))}
</OptGroup> </OptGroup>
); );
})} })}
{ungroupedOptions.length > 0 && ( {ungroupedOptions.length > 0 && (
<OptGroup label="其他"> <OptGroup label="其他">
{ungroupedOptions.map((opt) => ( {ungroupedOptions.map((opt) => (
<Option key={opt.value} value={opt.value} label={opt.label}> <Option key={opt.value} value={opt.value} label={opt.label}>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<span>{getControlDisplayName(opt.value)}</span> <span>{getControlDisplayName(opt.value)}</span>
<Tooltip title={opt.description}> <Tooltip title={opt.description}>
<InfoCircleOutlined <InfoCircleOutlined
style={{ color: "#8c8c8c", fontSize: 12 }} style={{ color: "#8c8c8c", fontSize: 12 }}
/> />
</Tooltip> </Tooltip>
</div> </div>
</Option> </Option>
))} ))}
</OptGroup> </OptGroup>
)} )}
</Select> </Select>
); );
} }
// Objects selector (no grouping) // Objects selector (no grouping)
return ( return (
<Select <Select
value={value} value={value}
onChange={onChange} onChange={onChange}
placeholder={placeholder || "选择数据对象类型"} placeholder={placeholder || "选择数据对象类型"}
style={style} style={style}
disabled={disabled} disabled={disabled}
showSearch showSearch
optionFilterProp="label" optionFilterProp="label"
> >
{tagOptions.map((opt) => ( {tagOptions.map((opt) => (
<Option key={opt.value} value={opt.value} label={opt.label}> <Option key={opt.value} value={opt.value} label={opt.label}>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<span>{getObjectDisplayName(opt.value)}</span> <span>{getObjectDisplayName(opt.value)}</span>
<Tooltip title={opt.description}> <Tooltip title={opt.description}>
<InfoCircleOutlined style={{ color: "#8c8c8c", fontSize: 12 }} /> <InfoCircleOutlined style={{ color: "#8c8c8c", fontSize: 12 }} />
</Tooltip> </Tooltip>
</div> </div>
</Option> </Option>
))} ))}
</Select> </Select>
); );
}; };
export default TagSelector; export default TagSelector;
/** /**
* Tag Info Panel Component * Tag Info Panel Component
* Displays detailed information about a selected tag * Displays detailed information about a selected tag
*/ */
interface TagInfoPanelProps { interface TagInfoPanelProps {
tagConfig: LabelStudioTagConfig | null; tagConfig: LabelStudioTagConfig | null;
tagType: string; tagType: string;
category: "object" | "control"; category: "object" | "control";
} }
export const TagInfoPanel: React.FC<TagInfoPanelProps> = ({ export const TagInfoPanel: React.FC<TagInfoPanelProps> = ({
tagConfig, tagConfig,
tagType, tagType,
category, category,
}) => { }) => {
if (!tagConfig || !tagType) { if (!tagConfig || !tagType) {
return null; return null;
} }
const config = const config =
category === "object" category === "object"
? tagConfig.objects[tagType] ? tagConfig.objects[tagType]
: tagConfig.controls[tagType]; : tagConfig.controls[tagType];
if (!config) { if (!config) {
return null; return null;
} }
return ( return (
<Collapse <Collapse
size="small" size="small"
items={[ items={[
{ {
key: "1", key: "1",
label: "标签配置详情", label: "标签配置详情",
children: ( children: (
<Space direction="vertical" size="small" style={{ width: "100%" }}> <Space direction="vertical" size="small" style={{ width: "100%" }}>
<div> <div>
<strong></strong> <strong></strong>
{config.description} {config.description}
</div> </div>
<div> <div>
<strong></strong> <strong></strong>
<div style={{ marginTop: 4 }}> <div style={{ marginTop: 4 }}>
{config.required_attrs.map((attr: string) => ( {config.required_attrs.map((attr: string) => (
<Tag key={attr} color="red"> <Tag key={attr} color="red">
{attr} {attr}
</Tag> </Tag>
))} ))}
</div> </div>
</div> </div>
{config.optional_attrs && {config.optional_attrs &&
Object.keys(config.optional_attrs).length > 0 && ( Object.keys(config.optional_attrs).length > 0 && (
<div> <div>
<strong></strong> <strong></strong>
<div style={{ marginTop: 4 }}> <div style={{ marginTop: 4 }}>
{Object.entries(config.optional_attrs).map( {Object.entries(config.optional_attrs).map(
([attrName, attrConfig]: [string, any]) => ( ([attrName, attrConfig]: [string, any]) => (
<Tooltip <Tooltip
key={attrName} key={attrName}
title={ title={
<div> <div>
{attrConfig.description && ( {attrConfig.description && (
<div>{attrConfig.description}</div> <div>{attrConfig.description}</div>
)} )}
{attrConfig.type && ( {attrConfig.type && (
<div>: {attrConfig.type}</div> <div>: {attrConfig.type}</div>
)} )}
{attrConfig.default !== undefined && ( {attrConfig.default !== undefined && (
<div>: {String(attrConfig.default)}</div> <div>: {String(attrConfig.default)}</div>
)} )}
{attrConfig.values && ( {attrConfig.values && (
<div> <div>
: {attrConfig.values.join(", ")} : {attrConfig.values.join(", ")}
</div> </div>
)} )}
</div> </div>
} }
> >
<Tag color="blue" style={{ cursor: "help" }}> <Tag color="blue" style={{ cursor: "help" }}>
{attrName} {attrName}
</Tag> </Tag>
</Tooltip> </Tooltip>
) )
)} )}
</div> </div>
</div> </div>
)} )}
{config.requires_children && ( {config.requires_children && (
<div> <div>
<strong></strong> <strong></strong>
<Tag color="green"> &lt;{config.child_tag}&gt;</Tag> <Tag color="green"> &lt;{config.child_tag}&gt;</Tag>
</div> </div>
)} )}
</Space> </Space>
), ),
}, },
]} ]}
/> />
); );
}; };

View File

@@ -1,3 +1,3 @@
export { default as TagSelector } from "./TagSelector"; export { default as TagSelector } from "./TagSelector";
export { default as TagBrowser } from "./TagBrowser"; export { default as TagBrowser } from "./TagBrowser";
export { TagInfoPanel } from "./TagSelector"; export { TagInfoPanel } from "./TagSelector";

View File

@@ -1,4 +1,4 @@
export { default as TemplateList } from "./TemplateList"; export { default as TemplateList } from "./TemplateList";
export { default as TemplateForm } from "./TemplateForm"; export { default as TemplateForm } from "./TemplateForm";
export { default as TemplateDetail } from "./TemplateDetail"; export { default as TemplateDetail } from "./TemplateDetail";
export { TagBrowser, TagSelector, TagInfoPanel } from "./components"; export { TagBrowser, TagSelector, TagInfoPanel } from "./components";

View File

@@ -1,50 +1,50 @@
import { get, post, put, del } from "@/utils/request"; import { get, post, put, del } from "@/utils/request";
// 标注任务管理相关接口 // 标注任务管理相关接口
export function queryAnnotationTasksUsingGet(params?: any) { export function queryAnnotationTasksUsingGet(params?: any) {
return get("/api/annotation/project", params); return get("/api/annotation/project", params);
} }
export function createAnnotationTaskUsingPost(data: any) { export function createAnnotationTaskUsingPost(data: any) {
return post("/api/annotation/project", data); return post("/api/annotation/project", data);
} }
export function syncAnnotationTaskUsingPost(data: any) { export function syncAnnotationTaskUsingPost(data: any) {
return post(`/api/annotation/task/sync`, data); return post(`/api/annotation/task/sync`, data);
} }
export function deleteAnnotationTaskByIdUsingDelete(mappingId: string) { export function deleteAnnotationTaskByIdUsingDelete(mappingId: string) {
// Backend expects mapping UUID as path parameter // Backend expects mapping UUID as path parameter
return del(`/api/annotation/project/${mappingId}`); return del(`/api/annotation/project/${mappingId}`);
} }
export function loginAnnotationUsingGet(mappingId: string) { export function loginAnnotationUsingGet(mappingId: string) {
return get("/api/annotation/project/${mappingId}/login"); return get("/api/annotation/project/${mappingId}/login");
} }
// 标签配置管理 // 标签配置管理
export function getTagConfigUsingGet() { export function getTagConfigUsingGet() {
return get("/api/annotation/tags/config"); return get("/api/annotation/tags/config");
} }
// 标注模板管理 // 标注模板管理
export function queryAnnotationTemplatesUsingGet(params?: any) { export function queryAnnotationTemplatesUsingGet(params?: any) {
return get("/api/annotation/template", params); return get("/api/annotation/template", params);
} }
export function createAnnotationTemplateUsingPost(data: any) { export function createAnnotationTemplateUsingPost(data: any) {
return post("/api/annotation/template", data); return post("/api/annotation/template", data);
} }
export function updateAnnotationTemplateByIdUsingPut( export function updateAnnotationTemplateByIdUsingPut(
templateId: string | number, templateId: string | number,
data: any data: any
) { ) {
return put(`/api/annotation/template/${templateId}`, data); return put(`/api/annotation/template/${templateId}`, data);
} }
export function deleteAnnotationTemplateByIdUsingDelete( export function deleteAnnotationTemplateByIdUsingDelete(
templateId: string | number templateId: string | number
) { ) {
return del(`/api/annotation/template/${templateId}`); return del(`/api/annotation/template/${templateId}`);
} }

View File

@@ -1,140 +1,140 @@
import { StickyNote } from "lucide-react"; import { StickyNote } from "lucide-react";
import {AnnotationTaskStatus, AnnotationType, Classification, DataType, TemplateType} from "./annotation.model"; import {AnnotationTaskStatus, AnnotationType, Classification, DataType, TemplateType} from "./annotation.model";
import { import {
CheckCircleOutlined, CheckCircleOutlined,
ClockCircleOutlined, ClockCircleOutlined,
CloseCircleOutlined, CloseCircleOutlined,
} from "@ant-design/icons"; } from "@ant-design/icons";
export const AnnotationTaskStatusMap = { export const AnnotationTaskStatusMap = {
[AnnotationTaskStatus.ACTIVE]: { [AnnotationTaskStatus.ACTIVE]: {
label: "活跃", label: "活跃",
value: AnnotationTaskStatus.ACTIVE, value: AnnotationTaskStatus.ACTIVE,
color: "#409f17ff", color: "#409f17ff",
icon: <CheckCircleOutlined />, icon: <CheckCircleOutlined />,
}, },
[AnnotationTaskStatus.PROCESSING]: { [AnnotationTaskStatus.PROCESSING]: {
label: "处理中", label: "处理中",
value: AnnotationTaskStatus.PROCESSING, value: AnnotationTaskStatus.PROCESSING,
color: "#2673e5", color: "#2673e5",
icon: <ClockCircleOutlined />, icon: <ClockCircleOutlined />,
}, },
[AnnotationTaskStatus.INACTIVE]: { [AnnotationTaskStatus.INACTIVE]: {
label: "未激活", label: "未激活",
value: AnnotationTaskStatus.INACTIVE, value: AnnotationTaskStatus.INACTIVE,
color: "#4f4444ff", color: "#4f4444ff",
icon: <CloseCircleOutlined />, icon: <CloseCircleOutlined />,
}, },
}; };
export function mapAnnotationTask(task: any) { export function mapAnnotationTask(task: any) {
// Normalize labeling project id from possible backend field names // Normalize labeling project id from possible backend field names
const labelingProjId = task?.labelingProjId || task?.labelingProjectId || task?.projId || task?.labeling_project_id || ""; const labelingProjId = task?.labelingProjId || task?.labelingProjectId || task?.projId || task?.labeling_project_id || "";
const statsArray = task?.statistics const statsArray = task?.statistics
? [ ? [
{ label: "准确率", value: task.statistics.accuracy ?? "-" }, { label: "准确率", value: task.statistics.accuracy ?? "-" },
{ label: "平均时长", value: task.statistics.averageTime ?? "-" }, { label: "平均时长", value: task.statistics.averageTime ?? "-" },
{ label: "待复核", value: task.statistics.reviewCount ?? "-" }, { label: "待复核", value: task.statistics.reviewCount ?? "-" },
] ]
: []; : [];
return { return {
...task, ...task,
id: task.id, id: task.id,
// provide consistent field for components // provide consistent field for components
labelingProjId, labelingProjId,
projId: labelingProjId, projId: labelingProjId,
name: task.name, name: task.name,
description: task.description || "", description: task.description || "",
datasetName: task.datasetName || task.dataset_name || "-", datasetName: task.datasetName || task.dataset_name || "-",
createdAt: task.createdAt || task.created_at || "-", createdAt: task.createdAt || task.created_at || "-",
updatedAt: task.updatedAt || task.updated_at || "-", updatedAt: task.updatedAt || task.updated_at || "-",
icon: <StickyNote />, icon: <StickyNote />,
iconColor: "bg-blue-100", iconColor: "bg-blue-100",
status: { status: {
label: label:
task.status === "completed" task.status === "completed"
? "已完成" ? "已完成"
: task.status === "processing" : task.status === "processing"
? "进行中" ? "进行中"
: task.status === "skipped" : task.status === "skipped"
? "已跳过" ? "已跳过"
: "待开始", : "待开始",
color: "bg-blue-100", color: "bg-blue-100",
}, },
statistics: statsArray, statistics: statsArray,
}; };
} }
export const DataTypeMap = { export const DataTypeMap = {
[DataType.TEXT]: { [DataType.TEXT]: {
label: "文本", label: "文本",
value: DataType.TEXT value: DataType.TEXT
}, },
[DataType.IMAGE]: { [DataType.IMAGE]: {
label: "图片", label: "图片",
value: DataType.IMAGE value: DataType.IMAGE
}, },
[DataType.AUDIO]: { [DataType.AUDIO]: {
label: "音频", label: "音频",
value: DataType.AUDIO value: DataType.AUDIO
}, },
[DataType.VIDEO]: { [DataType.VIDEO]: {
label: "视频", label: "视频",
value: DataType.VIDEO value: DataType.VIDEO
}, },
} }
export const ClassificationMap = { export const ClassificationMap = {
[Classification.COMPUTER_VERSION]: { [Classification.COMPUTER_VERSION]: {
label: "计算机视觉", label: "计算机视觉",
value: Classification.COMPUTER_VERSION value: Classification.COMPUTER_VERSION
}, },
[Classification.NLP]: { [Classification.NLP]: {
label: "自然语言处理", label: "自然语言处理",
value: Classification.NLP value: Classification.NLP
}, },
[Classification.AUDIO]: { [Classification.AUDIO]: {
label: "音频", label: "音频",
value: Classification.AUDIO value: Classification.AUDIO
}, },
[Classification.QUALITY_CONTROL]: { [Classification.QUALITY_CONTROL]: {
label: "质量控制", label: "质量控制",
value: Classification.QUALITY_CONTROL value: Classification.QUALITY_CONTROL
}, },
[Classification.CUSTOM]: { [Classification.CUSTOM]: {
label: "自定义", label: "自定义",
value: Classification.CUSTOM value: Classification.CUSTOM
}, },
} }
export const AnnotationTypeMap = { export const AnnotationTypeMap = {
[AnnotationType.CLASSIFICATION]: { [AnnotationType.CLASSIFICATION]: {
label: "分类", label: "分类",
value: AnnotationType.CLASSIFICATION value: AnnotationType.CLASSIFICATION
}, },
[AnnotationType.OBJECT_DETECTION]: { [AnnotationType.OBJECT_DETECTION]: {
label: "目标检测", label: "目标检测",
value: AnnotationType.OBJECT_DETECTION value: AnnotationType.OBJECT_DETECTION
}, },
[AnnotationType.SEGMENTATION]: { [AnnotationType.SEGMENTATION]: {
label: "分割", label: "分割",
value: AnnotationType.SEGMENTATION value: AnnotationType.SEGMENTATION
}, },
[AnnotationType.NER]: { [AnnotationType.NER]: {
label: "命名实体识别", label: "命名实体识别",
value: AnnotationType.NER value: AnnotationType.NER
}, },
} }
export const TemplateTypeMap = { export const TemplateTypeMap = {
[TemplateType.SYSTEM]: { [TemplateType.SYSTEM]: {
label: "系统内置", label: "系统内置",
value: TemplateType.SYSTEM value: TemplateType.SYSTEM
}, },
[TemplateType.CUSTOM]: { [TemplateType.CUSTOM]: {
label: "自定义", label: "自定义",
value: TemplateType.CUSTOM value: TemplateType.CUSTOM
}, },
} }

View File

@@ -1,107 +1,107 @@
import type { DatasetType } from "@/pages/DataManagement/dataset.model"; import type { DatasetType } from "@/pages/DataManagement/dataset.model";
export enum AnnotationTaskStatus { export enum AnnotationTaskStatus {
ACTIVE = "active", ACTIVE = "active",
INACTIVE = "inactive", INACTIVE = "inactive",
PROCESSING = "processing", PROCESSING = "processing",
COMPLETED = "completed", COMPLETED = "completed",
SKIPPED = "skipped", SKIPPED = "skipped",
} }
export interface AnnotationTask { export interface AnnotationTask {
id: string; id: string;
name: string; name: string;
labelingProjId: string; labelingProjId: string;
datasetId: string; datasetId: string;
annotationCount: number; annotationCount: number;
description?: string; description?: string;
assignedTo?: string; assignedTo?: string;
progress: number; progress: number;
statistics: { statistics: {
accuracy: number; accuracy: number;
averageTime: number; averageTime: number;
reviewCount: number; reviewCount: number;
}; };
status: AnnotationTaskStatus; status: AnnotationTaskStatus;
totalDataCount: number; totalDataCount: number;
type: DatasetType; type: DatasetType;
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
} }
// 标注模板相关类型 // 标注模板相关类型
export interface LabelDefinition { export interface LabelDefinition {
fromName: string; fromName: string;
toName: string; toName: string;
type: string; type: string;
options?: string[]; options?: string[];
labels?: string[]; labels?: string[];
required?: boolean; required?: boolean;
description?: string; description?: string;
} }
export interface ObjectDefinition { export interface ObjectDefinition {
name: string; name: string;
type: string; type: string;
value: string; value: string;
} }
export interface TemplateConfiguration { export interface TemplateConfiguration {
labels: LabelDefinition[]; labels: LabelDefinition[];
objects: ObjectDefinition[]; objects: ObjectDefinition[];
metadata?: Record<string, any>; metadata?: Record<string, any>;
} }
export interface AnnotationTemplate { export interface AnnotationTemplate {
id: string; id: string;
name: string; name: string;
description?: string; description?: string;
dataType: string; dataType: string;
labelingType: string; labelingType: string;
configuration: TemplateConfiguration; configuration: TemplateConfiguration;
labelConfig?: string; labelConfig?: string;
style: string; style: string;
category: string; category: string;
builtIn: boolean; builtIn: boolean;
version: string; version: string;
createdAt: string; createdAt: string;
updatedAt?: string; updatedAt?: string;
} }
export interface AnnotationTemplateListResponse { export interface AnnotationTemplateListResponse {
content: AnnotationTemplate[]; content: AnnotationTemplate[];
total: number; total: number;
page: number; page: number;
size: number; size: number;
totalPages: number; totalPages: number;
} }
export enum DataType { export enum DataType {
TEXT = "text", TEXT = "text",
IMAGE = "image", IMAGE = "image",
AUDIO = "audio", AUDIO = "audio",
VIDEO = "video", VIDEO = "video",
} }
export enum Classification { export enum Classification {
COMPUTER_VERSION = "computer-vision", COMPUTER_VERSION = "computer-vision",
NLP = "nlp", NLP = "nlp",
AUDIO = "audio", AUDIO = "audio",
QUALITY_CONTROL = "quality-control", QUALITY_CONTROL = "quality-control",
CUSTOM = "custom" CUSTOM = "custom"
} }
export enum AnnotationType { export enum AnnotationType {
CLASSIFICATION = "classification", CLASSIFICATION = "classification",
OBJECT_DETECTION = "object-detection", OBJECT_DETECTION = "object-detection",
SEGMENTATION = "segmentation", SEGMENTATION = "segmentation",
NER = "ner" NER = "ner"
} }
export enum TemplateType { export enum TemplateType {
SYSTEM = "true", SYSTEM = "true",
CUSTOM = "false" CUSTOM = "false"
} }

View File

@@ -1,187 +1,187 @@
/** /**
* Label Studio Tag Configuration Types * Label Studio Tag Configuration Types
* Corresponds to runtime/datamate-python/app/module/annotation/config/label_studio_tags.yaml * Corresponds to runtime/datamate-python/app/module/annotation/config/label_studio_tags.yaml
*/ */
export interface TagAttributeConfig { export interface TagAttributeConfig {
type?: "boolean" | "number" | "string"; type?: "boolean" | "number" | "string";
values?: string[]; values?: string[];
default?: any; default?: any;
description?: string; description?: string;
} }
export interface TagConfig { export interface TagConfig {
description: string; description: string;
required_attrs: string[]; required_attrs: string[];
optional_attrs?: Record<string, TagAttributeConfig>; optional_attrs?: Record<string, TagAttributeConfig>;
requires_children?: boolean; requires_children?: boolean;
child_tag?: string; child_tag?: string;
child_required_attrs?: string[]; child_required_attrs?: string[];
category?: string; // e.g., "labeling" or "layout" for controls; "image", "text", etc. for objects category?: string; // e.g., "labeling" or "layout" for controls; "image", "text", etc. for objects
} }
export interface LabelStudioTagConfig { export interface LabelStudioTagConfig {
objects: Record<string, TagConfig>; objects: Record<string, TagConfig>;
controls: Record<string, TagConfig>; controls: Record<string, TagConfig>;
} }
/** /**
* UI-friendly representation of a tag for selection * UI-friendly representation of a tag for selection
*/ */
export interface TagOption { export interface TagOption {
value: string; value: string;
label: string; label: string;
description: string; description: string;
category: "object" | "control"; category: "object" | "control";
requiresChildren: boolean; requiresChildren: boolean;
childTag?: string; childTag?: string;
requiredAttrs: string[]; requiredAttrs: string[];
optionalAttrs?: Record<string, TagAttributeConfig>; optionalAttrs?: Record<string, TagAttributeConfig>;
} }
/** /**
* Convert backend tag config to frontend tag options * Convert backend tag config to frontend tag options
* @param config - The full tag configuration from backend * @param config - The full tag configuration from backend
* @param includeLabelingOnly - If true, only include controls with category="labeling" (default: true) * @param includeLabelingOnly - If true, only include controls with category="labeling" (default: true)
*/ */
export function parseTagConfig( export function parseTagConfig(
config: LabelStudioTagConfig, config: LabelStudioTagConfig,
includeLabelingOnly: boolean = true includeLabelingOnly: boolean = true
): { ): {
objectOptions: TagOption[]; objectOptions: TagOption[];
controlOptions: TagOption[]; controlOptions: TagOption[];
} { } {
const objectOptions: TagOption[] = Object.entries(config.objects).map( const objectOptions: TagOption[] = Object.entries(config.objects).map(
([key, value]) => ({ ([key, value]) => ({
value: key, value: key,
label: key, label: key,
description: value.description, description: value.description,
category: "object" as const, category: "object" as const,
requiresChildren: value.requires_children || false, requiresChildren: value.requires_children || false,
childTag: value.child_tag, childTag: value.child_tag,
requiredAttrs: value.required_attrs, requiredAttrs: value.required_attrs,
optionalAttrs: value.optional_attrs, optionalAttrs: value.optional_attrs,
}) })
); );
const controlOptions: TagOption[] = Object.entries(config.controls) const controlOptions: TagOption[] = Object.entries(config.controls)
.filter(([_, value]) => { .filter(([_, value]) => {
// If includeLabelingOnly is true, filter out layout controls // If includeLabelingOnly is true, filter out layout controls
if (includeLabelingOnly) { if (includeLabelingOnly) {
return value.category === "labeling"; return value.category === "labeling";
} }
return true; return true;
}) })
.map(([key, value]) => ({ .map(([key, value]) => ({
value: key, value: key,
label: key, label: key,
description: value.description, description: value.description,
category: "control" as const, category: "control" as const,
requiresChildren: value.requires_children || false, requiresChildren: value.requires_children || false,
childTag: value.child_tag, childTag: value.child_tag,
requiredAttrs: value.required_attrs, requiredAttrs: value.required_attrs,
optionalAttrs: value.optional_attrs, optionalAttrs: value.optional_attrs,
})); }));
return { objectOptions, controlOptions }; return { objectOptions, controlOptions };
} }
/** /**
* Get user-friendly display name for control types * Get user-friendly display name for control types
*/ */
export function getControlDisplayName(controlType: string): string { export function getControlDisplayName(controlType: string): string {
const displayNames: Record<string, string> = { const displayNames: Record<string, string> = {
Choices: "选项 (单选/多选)", Choices: "选项 (单选/多选)",
RectangleLabels: "矩形框", RectangleLabels: "矩形框",
PolygonLabels: "多边形", PolygonLabels: "多边形",
Labels: "标签", Labels: "标签",
TextArea: "文本区域", TextArea: "文本区域",
Rating: "评分", Rating: "评分",
Taxonomy: "分类树", Taxonomy: "分类树",
Ranker: "排序", Ranker: "排序",
List: "列表", List: "列表",
BrushLabels: "画笔分割", BrushLabels: "画笔分割",
EllipseLabels: "椭圆", EllipseLabels: "椭圆",
KeyPointLabels: "关键点", KeyPointLabels: "关键点",
Rectangle: "矩形", Rectangle: "矩形",
Polygon: "多边形", Polygon: "多边形",
Ellipse: "椭圆", Ellipse: "椭圆",
KeyPoint: "关键点", KeyPoint: "关键点",
Brush: "画笔", Brush: "画笔",
Number: "数字输入", Number: "数字输入",
DateTime: "日期时间", DateTime: "日期时间",
Relation: "关系", Relation: "关系",
Relations: "关系组", Relations: "关系组",
Pairwise: "成对比较", Pairwise: "成对比较",
}; };
return displayNames[controlType] || controlType; return displayNames[controlType] || controlType;
} }
/** /**
* Get user-friendly display name for object types * Get user-friendly display name for object types
*/ */
export function getObjectDisplayName(objectType: string): string { export function getObjectDisplayName(objectType: string): string {
const displayNames: Record<string, string> = { const displayNames: Record<string, string> = {
Image: "图像", Image: "图像",
Text: "文本", Text: "文本",
Audio: "音频", Audio: "音频",
Video: "视频", Video: "视频",
HyperText: "HTML内容", HyperText: "HTML内容",
PDF: "PDF文档", PDF: "PDF文档",
Markdown: "Markdown内容", Markdown: "Markdown内容",
Paragraphs: "段落", Paragraphs: "段落",
Table: "表格", Table: "表格",
AudioPlus: "高级音频", AudioPlus: "高级音频",
Timeseries: "时间序列", Timeseries: "时间序列",
Vector: "向量数据", Vector: "向量数据",
Chat: "对话数据", Chat: "对话数据",
}; };
return displayNames[objectType] || objectType; return displayNames[objectType] || objectType;
} }
/** /**
* Group control types by common usage patterns * Group control types by common usage patterns
*/ */
export function getControlGroups(): Record< export function getControlGroups(): Record<
string, string,
{ label: string; controls: string[] } { label: string; controls: string[] }
> { > {
return { return {
classification: { classification: {
label: "分类标注", label: "分类标注",
controls: ["Choices", "Taxonomy", "Labels", "Rating"], controls: ["Choices", "Taxonomy", "Labels", "Rating"],
}, },
detection: { detection: {
label: "目标检测", label: "目标检测",
controls: [ controls: [
"RectangleLabels", "RectangleLabels",
"PolygonLabels", "PolygonLabels",
"EllipseLabels", "EllipseLabels",
"KeyPointLabels", "KeyPointLabels",
"Rectangle", "Rectangle",
"Polygon", "Polygon",
"Ellipse", "Ellipse",
"KeyPoint", "KeyPoint",
], ],
}, },
segmentation: { segmentation: {
label: "分割标注", label: "分割标注",
controls: ["BrushLabels", "Brush", "BitmaskLabels", "MagicWand"], controls: ["BrushLabels", "Brush", "BitmaskLabels", "MagicWand"],
}, },
text: { text: {
label: "文本输入", label: "文本输入",
controls: ["TextArea", "Number", "DateTime"], controls: ["TextArea", "Number", "DateTime"],
}, },
other: { other: {
label: "其他", label: "其他",
controls: [ controls: [
"TimeseriesLabels", "TimeseriesLabels",
"VectorLabels", "VectorLabels",
"ParagraphLabels", "ParagraphLabels",
"VideoRectangle", "VideoRectangle",
], ],
}, },
}; };
} }

View File

@@ -1,133 +1,133 @@
import { useState } from "react"; import { useState } from "react";
import { Steps, Button, message, Form } from "antd"; import { Steps, Button, message, Form } from "antd";
import { SaveOutlined } from "@ant-design/icons"; import { SaveOutlined } from "@ant-design/icons";
import { Link, useNavigate } from "react-router"; import { Link, useNavigate } from "react-router";
import { ArrowLeft } from "lucide-react"; import { ArrowLeft } from "lucide-react";
import { createCleaningTaskUsingPost } from "../cleansing.api"; import { createCleaningTaskUsingPost } from "../cleansing.api";
import CreateTaskStepOne from "./components/CreateTaskStepOne"; import CreateTaskStepOne from "./components/CreateTaskStepOne";
import { useCreateStepTwo } from "./hooks/useCreateStepTwo"; import { useCreateStepTwo } from "./hooks/useCreateStepTwo";
import { DatasetType } from "@/pages/DataManagement/dataset.model"; import { DatasetType } from "@/pages/DataManagement/dataset.model";
export default function CleansingTaskCreate() { export default function CleansingTaskCreate() {
const navigate = useNavigate(); const navigate = useNavigate();
const [form] = Form.useForm(); const [form] = Form.useForm();
const [taskConfig, setTaskConfig] = useState({ const [taskConfig, setTaskConfig] = useState({
name: "", name: "",
description: "", description: "",
srcDatasetId: "", srcDatasetId: "",
srcDatasetName: "", srcDatasetName: "",
destDatasetName: "", destDatasetName: "",
destDatasetType: DatasetType.TEXT, destDatasetType: DatasetType.TEXT,
type: DatasetType.TEXT, type: DatasetType.TEXT,
}); });
const { const {
renderStepTwo, renderStepTwo,
selectedOperators, selectedOperators,
currentStep, currentStep,
handlePrev, handlePrev,
handleNext, handleNext,
} = useCreateStepTwo(); } = useCreateStepTwo();
const handleSave = async () => { const handleSave = async () => {
const task = { const task = {
...taskConfig, ...taskConfig,
instance: selectedOperators.map((item) => ({ instance: selectedOperators.map((item) => ({
id: item.id, id: item.id,
overrides: { overrides: {
...item.defaultParams, ...item.defaultParams,
...item.overrides, ...item.overrides,
}, },
inputs: item.inputs, inputs: item.inputs,
outputs: item.outputs, outputs: item.outputs,
})), })),
}; };
navigate("/data/cleansing?view=task"); navigate("/data/cleansing?view=task");
await createCleaningTaskUsingPost(task); await createCleaningTaskUsingPost(task);
message.success("任务已创建"); message.success("任务已创建");
}; };
const canProceed = () => { const canProceed = () => {
switch (currentStep) { switch (currentStep) {
case 1: { case 1: {
const values = form.getFieldsValue(); const values = form.getFieldsValue();
return ( return (
values.name && values.name &&
values.srcDatasetId && values.srcDatasetId &&
values.destDatasetName && values.destDatasetName &&
values.destDatasetType values.destDatasetType
); );
} }
case 2: case 2:
return selectedOperators.length > 0; return selectedOperators.length > 0;
default: default:
return false; return false;
} }
}; };
const renderStepContent = () => { const renderStepContent = () => {
switch (currentStep) { switch (currentStep) {
case 1: case 1:
return ( return (
<CreateTaskStepOne <CreateTaskStepOne
form={form} form={form}
taskConfig={taskConfig} taskConfig={taskConfig}
setTaskConfig={setTaskConfig} setTaskConfig={setTaskConfig}
/> />
); );
case 2: case 2:
return renderStepTwo; return renderStepTwo;
default: default:
return null; return null;
} }
}; };
return ( return (
<div className="h-full flex flex-col"> <div className="h-full flex flex-col">
{/* Header */} {/* Header */}
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
<div className="flex items-center"> <div className="flex items-center">
<Link to="/data/cleansing"> <Link to="/data/cleansing">
<Button type="text"> <Button type="text">
<ArrowLeft className="w-4 h-4 mr-1" /> <ArrowLeft className="w-4 h-4 mr-1" />
</Button> </Button>
</Link> </Link>
<h1 className="text-xl font-bold"></h1> <h1 className="text-xl font-bold"></h1>
</div> </div>
<div className="w-1/2"> <div className="w-1/2">
<Steps <Steps
size="small" size="small"
current={currentStep - 1} current={currentStep - 1}
items={[{ title: "基本信息" }, { title: "算子编排" }]} items={[{ title: "基本信息" }, { title: "算子编排" }]}
/> />
</div> </div>
</div> </div>
{/* Step Content */} {/* Step Content */}
<div className="flex-overflow-auto bg-white border-card"> <div className="flex-overflow-auto bg-white border-card">
<div className="flex-1 overflow-auto m-6">{renderStepContent()}</div> <div className="flex-1 overflow-auto m-6">{renderStepContent()}</div>
<div className="flex justify-end p-6 gap-3 border-top"> <div className="flex justify-end p-6 gap-3 border-top">
<Button onClick={() => navigate("/data/cleansing")}></Button> <Button onClick={() => navigate("/data/cleansing")}></Button>
{currentStep > 1 && <Button onClick={handlePrev}></Button>} {currentStep > 1 && <Button onClick={handlePrev}></Button>}
{currentStep === 2 ? ( {currentStep === 2 ? (
<Button <Button
type="primary" type="primary"
icon={<SaveOutlined />} icon={<SaveOutlined />}
onClick={handleSave} onClick={handleSave}
disabled={!canProceed()} disabled={!canProceed()}
> >
</Button> </Button>
) : ( ) : (
<Button <Button
type="primary" type="primary"
onClick={handleNext} onClick={handleNext}
disabled={!canProceed()} disabled={!canProceed()}
> >
</Button> </Button>
)} )}
</div> </div>
</div> </div>
</div> </div>
); );
} }

View File

@@ -1,142 +1,142 @@
import {useEffect, useState} from "react"; import {useEffect, useState} from "react";
import {Button, Steps, Form, message} from "antd"; import {Button, Steps, Form, message} from "antd";
import {Link, useNavigate, useParams} from "react-router"; import {Link, useNavigate, useParams} from "react-router";
import { ArrowLeft } from "lucide-react"; import { ArrowLeft } from "lucide-react";
import { import {
createCleaningTemplateUsingPost, createCleaningTemplateUsingPost,
queryCleaningTemplateByIdUsingGet, queryCleaningTemplateByIdUsingGet,
updateCleaningTemplateByIdUsingPut updateCleaningTemplateByIdUsingPut
} from "../cleansing.api"; } from "../cleansing.api";
import CleansingTemplateStepOne from "./components/CreateTemplateStepOne"; import CleansingTemplateStepOne from "./components/CreateTemplateStepOne";
import { useCreateStepTwo } from "./hooks/useCreateStepTwo"; import { useCreateStepTwo } from "./hooks/useCreateStepTwo";
export default function CleansingTemplateCreate() { export default function CleansingTemplateCreate() {
const { id = "" } = useParams() const { id = "" } = useParams()
const navigate = useNavigate(); const navigate = useNavigate();
const [form] = Form.useForm(); const [form] = Form.useForm();
const [templateConfig, setTemplateConfig] = useState({ const [templateConfig, setTemplateConfig] = useState({
name: "", name: "",
description: "", description: "",
}); });
const fetchTemplateDetail = async () => { const fetchTemplateDetail = async () => {
if (!id) return; if (!id) return;
try { try {
const { data } = await queryCleaningTemplateByIdUsingGet(id); const { data } = await queryCleaningTemplateByIdUsingGet(id);
setTemplateConfig(data); setTemplateConfig(data);
} catch (error) { } catch (error) {
message.error("获取任务详情失败"); message.error("获取任务详情失败");
navigate("/data/cleansing"); navigate("/data/cleansing");
} }
}; };
useEffect(() => { useEffect(() => {
fetchTemplateDetail() fetchTemplateDetail()
}, [id]); }, [id]);
const handleSave = async () => { const handleSave = async () => {
const template = { const template = {
...templateConfig, ...templateConfig,
instance: selectedOperators.map((item) => ({ instance: selectedOperators.map((item) => ({
id: item.id, id: item.id,
overrides: { overrides: {
...item.defaultParams, ...item.defaultParams,
...item.overrides, ...item.overrides,
}, },
inputs: item.inputs, inputs: item.inputs,
outputs: item.outputs, outputs: item.outputs,
})), })),
}; };
!id && await createCleaningTemplateUsingPost(template) && message.success("模板创建成功"); !id && await createCleaningTemplateUsingPost(template) && message.success("模板创建成功");
id && await updateCleaningTemplateByIdUsingPut(id, template) && message.success("模板更新成功"); id && await updateCleaningTemplateByIdUsingPut(id, template) && message.success("模板更新成功");
navigate("/data/cleansing?view=template"); navigate("/data/cleansing?view=template");
}; };
const { const {
renderStepTwo, renderStepTwo,
selectedOperators, selectedOperators,
currentStep, currentStep,
handlePrev, handlePrev,
handleNext, handleNext,
} = useCreateStepTwo(); } = useCreateStepTwo();
const canProceed = () => { const canProceed = () => {
const values = form.getFieldsValue(); const values = form.getFieldsValue();
switch (currentStep) { switch (currentStep) {
case 1: case 1:
return values.name; return values.name;
case 2: case 2:
return selectedOperators.length > 0; return selectedOperators.length > 0;
default: default:
return false; return false;
} }
}; };
const renderStepContent = () => { const renderStepContent = () => {
switch (currentStep) { switch (currentStep) {
case 1: case 1:
return ( return (
<CleansingTemplateStepOne <CleansingTemplateStepOne
form={form} form={form}
templateConfig={templateConfig} templateConfig={templateConfig}
setTemplateConfig={setTemplateConfig} setTemplateConfig={setTemplateConfig}
/> />
); );
case 2: case 2:
return renderStepTwo; return renderStepTwo;
default: default:
return null; return null;
} }
}; };
return ( return (
<div className="h-full flex flex-col"> <div className="h-full flex flex-col">
{/* Header */} {/* Header */}
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
<div className="flex items-center"> <div className="flex items-center">
<Link to="/data/cleansing"> <Link to="/data/cleansing">
<Button type="text"> <Button type="text">
<ArrowLeft className="w-4 h-4 mr-1" /> <ArrowLeft className="w-4 h-4 mr-1" />
</Button> </Button>
</Link> </Link>
<h1 className="text-xl font-bold">{id ? '更新清洗模板' : '创建清洗模板'}</h1> <h1 className="text-xl font-bold">{id ? '更新清洗模板' : '创建清洗模板'}</h1>
</div> </div>
<div className="w-1/2"> <div className="w-1/2">
<Steps <Steps
size="small" size="small"
current={currentStep} current={currentStep}
items={[{ title: "基本信息" }, { title: "算子编排" }]} items={[{ title: "基本信息" }, { title: "算子编排" }]}
/> />
</div> </div>
</div> </div>
<div className="flex-overflow-auto border-card"> <div className="flex-overflow-auto border-card">
<div className="flex-1 overflow-auto m-6">{renderStepContent()}</div> <div className="flex-1 overflow-auto m-6">{renderStepContent()}</div>
<div className="flex justify-end p-6 gap-3 border-top"> <div className="flex justify-end p-6 gap-3 border-top">
<Button onClick={() => navigate("/data/cleansing")}></Button> <Button onClick={() => navigate("/data/cleansing")}></Button>
{currentStep > 1 && <Button onClick={handlePrev}></Button>} {currentStep > 1 && <Button onClick={handlePrev}></Button>}
{currentStep === 2 ? ( {currentStep === 2 ? (
<Button <Button
type="primary" type="primary"
onClick={handleSave} onClick={handleSave}
disabled={!canProceed()} disabled={!canProceed()}
> >
{id ? '更新模板' : '创建模板'} {id ? '更新模板' : '创建模板'}
</Button> </Button>
) : ( ) : (
<Button <Button
type="primary" type="primary"
onClick={handleNext} onClick={handleNext}
disabled={!canProceed()} disabled={!canProceed()}
> >
</Button> </Button>
)} )}
</div> </div>
</div> </div>
</div> </div>
); );
} }

View File

@@ -1,113 +1,113 @@
import RadioCard from "@/components/RadioCard"; import RadioCard from "@/components/RadioCard";
import { queryDatasetsUsingGet } from "@/pages/DataManagement/dataset.api"; import { queryDatasetsUsingGet } from "@/pages/DataManagement/dataset.api";
import { datasetTypes, mapDataset } from "@/pages/DataManagement/dataset.const"; import { datasetTypes, mapDataset } from "@/pages/DataManagement/dataset.const";
import { import {
Dataset, Dataset,
DatasetSubType, DatasetSubType,
DatasetType, DatasetType,
} from "@/pages/DataManagement/dataset.model"; } from "@/pages/DataManagement/dataset.model";
import { Input, Select, Form } from "antd"; import { Input, Select, Form } from "antd";
import TextArea from "antd/es/input/TextArea"; import TextArea from "antd/es/input/TextArea";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
export default function CreateTaskStepOne({ export default function CreateTaskStepOne({
form, form,
taskConfig, taskConfig,
setTaskConfig, setTaskConfig,
}: { }: {
form: any; form: any;
taskConfig: { taskConfig: {
name: string; name: string;
description: string; description: string;
datasetId: string; datasetId: string;
destDatasetName: string; destDatasetName: string;
type: DatasetType; type: DatasetType;
destDatasetType: DatasetSubType; destDatasetType: DatasetSubType;
}; };
setTaskConfig: (config: any) => void; setTaskConfig: (config: any) => void;
}) { }) {
const [datasets, setDatasets] = useState<Dataset[]>([]); const [datasets, setDatasets] = useState<Dataset[]>([]);
const fetchDatasets = async () => { const fetchDatasets = async () => {
const { data } = await queryDatasetsUsingGet({ page: 1, size: 1000 }); const { data } = await queryDatasetsUsingGet({ page: 1, size: 1000 });
setDatasets(data.content.map(mapDataset) || []); setDatasets(data.content.map(mapDataset) || []);
}; };
useEffect(() => { useEffect(() => {
fetchDatasets(); fetchDatasets();
}, []); }, []);
const handleValuesChange = (currentValue, allValues) => { const handleValuesChange = (currentValue, allValues) => {
const [key, value] = Object.entries(currentValue)[0]; const [key, value] = Object.entries(currentValue)[0];
let dataset = null; let dataset = null;
if (key === "srcDatasetId") { if (key === "srcDatasetId") {
dataset = datasets.find((d) => d.id === value); dataset = datasets.find((d) => d.id === value);
setTaskConfig({ setTaskConfig({
...taskConfig, ...taskConfig,
...allValues, ...allValues,
srcDatasetName: dataset?.name || "", srcDatasetName: dataset?.name || "",
}); });
} else { } else {
setTaskConfig({ ...taskConfig, ...allValues }); setTaskConfig({ ...taskConfig, ...allValues });
} }
}; };
return ( return (
<Form <Form
layout="vertical" layout="vertical"
form={form} form={form}
initialValues={taskConfig} initialValues={taskConfig}
onValuesChange={handleValuesChange} onValuesChange={handleValuesChange}
> >
<h2 className="font-medium text-gray-900 text-base mb-2"></h2> <h2 className="font-medium text-gray-900 text-base mb-2"></h2>
<Form.Item label="名称" name="name" required> <Form.Item label="名称" name="name" required>
<Input placeholder="输入清洗任务名称" /> <Input placeholder="输入清洗任务名称" />
</Form.Item> </Form.Item>
<Form.Item label="描述" name="description"> <Form.Item label="描述" name="description">
<TextArea placeholder="描述清洗任务的目标和要求" rows={4} /> <TextArea placeholder="描述清洗任务的目标和要求" rows={4} />
</Form.Item> </Form.Item>
<h2 className="font-medium text-gray-900 pt-6 mb-2 text-base"> <h2 className="font-medium text-gray-900 pt-6 mb-2 text-base">
</h2> </h2>
<Form.Item label="源数据集" name="srcDatasetId" required> <Form.Item label="源数据集" name="srcDatasetId" required>
<Select <Select
placeholder="请选择源数据集" placeholder="请选择源数据集"
options={datasets.map((dataset) => { options={datasets.map((dataset) => {
return { return {
label: ( label: (
<div className="flex items-center justify-between gap-3 py-2"> <div className="flex items-center justify-between gap-3 py-2">
<div className="flex items-center font-sm text-gray-900"> <div className="flex items-center font-sm text-gray-900">
<span className="mr-2">{dataset.icon}</span> <span className="mr-2">{dataset.icon}</span>
<span>{dataset.name}</span> <span>{dataset.name}</span>
</div> </div>
<div className="text-xs text-gray-500">{dataset.size}</div> <div className="text-xs text-gray-500">{dataset.size}</div>
</div> </div>
), ),
value: dataset.id, value: dataset.id,
}; };
})} })}
/> />
</Form.Item> </Form.Item>
<Form.Item label="目标数据集名称" name="destDatasetName" required> <Form.Item label="目标数据集名称" name="destDatasetName" required>
<Input placeholder="输入目标数据集名称" /> <Input placeholder="输入目标数据集名称" />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
label="目标数据集类型" label="目标数据集类型"
name="destDatasetType" name="destDatasetType"
rules={[{ required: true, message: "请选择目标数据集类型" }]} rules={[{ required: true, message: "请选择目标数据集类型" }]}
> >
<RadioCard <RadioCard
options={datasetTypes} options={datasetTypes}
value={taskConfig.destDatasetType} value={taskConfig.destDatasetType}
onChange={(type) => { onChange={(type) => {
form.setFieldValue("destDatasetType", type); form.setFieldValue("destDatasetType", type);
setTaskConfig({ setTaskConfig({
...taskConfig, ...taskConfig,
destDatasetType: type as DatasetSubType, destDatasetType: type as DatasetSubType,
}); });
}} }}
/> />
</Form.Item> </Form.Item>
</Form> </Form>
); );
} }

View File

@@ -1,44 +1,44 @@
import { Input, Form } from "antd"; import { Input, Form } from "antd";
import {useEffect} from "react"; import {useEffect} from "react";
const { TextArea } = Input; const { TextArea } = Input;
export default function CreateTemplateStepOne({ export default function CreateTemplateStepOne({
form, form,
templateConfig, templateConfig,
setTemplateConfig, setTemplateConfig,
}: { }: {
form: any; form: any;
templateConfig: { name: string; description: string; type: string }; templateConfig: { name: string; description: string; type: string };
setTemplateConfig: React.Dispatch< setTemplateConfig: React.Dispatch<
React.SetStateAction<{ name: string; description: string; type: string }> React.SetStateAction<{ name: string; description: string; type: string }>
>; >;
}) { }) {
const handleValuesChange = (_, allValues) => { const handleValuesChange = (_, allValues) => {
setTemplateConfig({ ...templateConfig, ...allValues }); setTemplateConfig({ ...templateConfig, ...allValues });
}; };
useEffect(() => { useEffect(() => {
form.setFieldsValue(templateConfig); form.setFieldsValue(templateConfig);
}, [templateConfig]); }, [templateConfig]);
return ( return (
<Form <Form
form={form} form={form}
layout="vertical" layout="vertical"
initialValues={templateConfig} initialValues={templateConfig}
onValuesChange={handleValuesChange} onValuesChange={handleValuesChange}
> >
<Form.Item <Form.Item
label="模板名称" label="模板名称"
name="name" name="name"
rules={[{ required: true, message: "请输入模板名称" }]} rules={[{ required: true, message: "请输入模板名称" }]}
> >
<Input placeholder="输入模板名称" /> <Input placeholder="输入模板名称" />
</Form.Item> </Form.Item>
<Form.Item label="模板描述" name="description"> <Form.Item label="模板描述" name="description">
<TextArea placeholder="描述模板的用途和特点" rows={4} /> <TextArea placeholder="描述模板的用途和特点" rows={4} />
</Form.Item> </Form.Item>
</Form> </Form>
); );
} }

View File

@@ -1,81 +1,81 @@
import React from "react"; import React from "react";
import { Tag, Divider, Form } from "antd"; import { Tag, Divider, Form } from "antd";
import ParamConfig from "./ParamConfig"; import ParamConfig from "./ParamConfig";
import { Settings } from "lucide-react"; import { Settings } from "lucide-react";
import { OperatorI } from "@/pages/OperatorMarket/operator.model"; import { OperatorI } from "@/pages/OperatorMarket/operator.model";
// OperatorConfig/OperatorTemplate 类型需根据主文件实际导入 // OperatorConfig/OperatorTemplate 类型需根据主文件实际导入
interface OperatorConfigProps { interface OperatorConfigProps {
selectedOp: OperatorI; selectedOp: OperatorI;
renderParamConfig?: ( renderParamConfig?: (
operator: OperatorI, operator: OperatorI,
paramKey: string, paramKey: string,
param: any param: any
) => React.ReactNode; ) => React.ReactNode;
handleConfigChange?: ( handleConfigChange?: (
operatorId: string, operatorId: string,
paramKey: string, paramKey: string,
value: any value: any
) => void; ) => void;
} }
const OperatorConfig: React.FC<OperatorConfigProps> = ({ const OperatorConfig: React.FC<OperatorConfigProps> = ({
selectedOp, selectedOp,
renderParamConfig, renderParamConfig,
handleConfigChange, handleConfigChange,
}) => { }) => {
return ( return (
<div className="w-1/4 min-w-3xs flex flex-col h-full"> <div className="w-1/4 min-w-3xs flex flex-col h-full">
<div className="px-4 pb-4 border-b border-gray-200"> <div className="px-4 pb-4 border-b border-gray-200">
<span className="font-semibold text-base flex items-center gap-2"> <span className="font-semibold text-base flex items-center gap-2">
<Settings /> <Settings />
</span> </span>
</div> </div>
<div className="flex-1 overflow-auto p-4"> <div className="flex-1 overflow-auto p-4">
{selectedOp ? ( {selectedOp ? (
<div> <div>
<div className="mb-4"> <div className="mb-4">
<div className="flex items-center gap-2 mb-1"> <div className="flex items-center gap-2 mb-1">
<span className="font-medium">{selectedOp.name}</span> <span className="font-medium">{selectedOp.name}</span>
</div> </div>
<div className="text-sm text-gray-500"> <div className="text-sm text-gray-500">
{selectedOp.description} {selectedOp.description}
</div> </div>
<div className="flex flex-wrap gap-1 mt-2"> <div className="flex flex-wrap gap-1 mt-2">
{selectedOp?.tags?.map((tag: string) => ( {selectedOp?.tags?.map((tag: string) => (
<Tag key={tag} color="default"> <Tag key={tag} color="default">
{tag} {tag}
</Tag> </Tag>
))} ))}
</div> </div>
</div> </div>
<Divider /> <Divider />
<Form layout="vertical"> <Form layout="vertical">
{Object.entries(selectedOp.configs).map(([key, param]) => {Object.entries(selectedOp.configs).map(([key, param]) =>
renderParamConfig ? ( renderParamConfig ? (
renderParamConfig(selectedOp, key, param) renderParamConfig(selectedOp, key, param)
) : ( ) : (
<ParamConfig <ParamConfig
key={key} key={key}
operator={selectedOp} operator={selectedOp}
paramKey={key} paramKey={key}
param={param} param={param}
onParamChange={handleConfigChange} onParamChange={handleConfigChange}
/> />
) )
)} )}
</Form> </Form>
</div> </div>
) : ( ) : (
<div className="text-center py-12 text-gray-400"> <div className="text-center py-12 text-gray-400">
<Settings className="w-full w-10 h-10 mb-4 opacity-50" /> <Settings className="w-full w-10 h-10 mb-4 opacity-50" />
<div></div> <div></div>
</div> </div>
)} )}
</div> </div>
</div> </div>
); );
}; };
export default OperatorConfig; export default OperatorConfig;

View File

@@ -1,287 +1,287 @@
import React, {useEffect, useMemo, useState} from "react"; import React, {useEffect, useMemo, useState} from "react";
import {Button, Card, Checkbox, Collapse, Input, Select, Tag, Tooltip,} from "antd"; import {Button, Card, Checkbox, Collapse, Input, Select, Tag, Tooltip,} from "antd";
import {SearchOutlined, StarFilled, StarOutlined} from "@ant-design/icons"; import {SearchOutlined, StarFilled, StarOutlined} from "@ant-design/icons";
import {CategoryI, OperatorI} from "@/pages/OperatorMarket/operator.model"; import {CategoryI, OperatorI} from "@/pages/OperatorMarket/operator.model";
import {Layers} from "lucide-react"; import {Layers} from "lucide-react";
import {updateOperatorByIdUsingPut} from "@/pages/OperatorMarket/operator.api.ts"; import {updateOperatorByIdUsingPut} from "@/pages/OperatorMarket/operator.api.ts";
interface OperatorListProps { interface OperatorListProps {
operators: OperatorI[]; operators: OperatorI[];
favorites: Set<string>; favorites: Set<string>;
showPoppular?: boolean; showPoppular?: boolean;
toggleFavorite: (id: string) => void; toggleFavorite: (id: string) => void;
toggleOperator: (operator: OperatorI) => void; toggleOperator: (operator: OperatorI) => void;
selectedOperators: OperatorI[]; selectedOperators: OperatorI[];
onDragOperator: ( onDragOperator: (
e: React.DragEvent, e: React.DragEvent,
item: OperatorI, item: OperatorI,
source: "library" source: "library"
) => void; ) => void;
} }
const handleStar = async (operator: OperatorI, toggleFavorite: (id: string) => void) => { const handleStar = async (operator: OperatorI, toggleFavorite: (id: string) => void) => {
const data = { const data = {
id: operator.id, id: operator.id,
isStar: !operator.isStar isStar: !operator.isStar
}; };
await updateOperatorByIdUsingPut(operator.id, data); await updateOperatorByIdUsingPut(operator.id, data);
toggleFavorite(operator.id) toggleFavorite(operator.id)
} }
const OperatorList: React.FC<OperatorListProps> = ({ const OperatorList: React.FC<OperatorListProps> = ({
operators, operators,
favorites, favorites,
toggleFavorite, toggleFavorite,
toggleOperator, toggleOperator,
selectedOperators, selectedOperators,
onDragOperator, onDragOperator,
}) => ( }) => (
<div className="grid grid-cols-1 gap-2"> <div className="grid grid-cols-1 gap-2">
{operators.map((operator) => { {operators.map((operator) => {
// 判断是否已选 // 判断是否已选
const isSelected = selectedOperators.some((op) => op.id === operator.id); const isSelected = selectedOperators.some((op) => op.id === operator.id);
return ( return (
<Card <Card
size="small" size="small"
key={operator.id} key={operator.id}
draggable draggable
hoverable hoverable
onDragStart={(e) => onDragOperator(e, operator, "library")} onDragStart={(e) => onDragOperator(e, operator, "library")}
onClick={() => toggleOperator(operator)} onClick={() => toggleOperator(operator)}
> >
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex flex-1 min-w-0 items-center gap-2"> <div className="flex flex-1 min-w-0 items-center gap-2">
<Checkbox checked={isSelected} /> <Checkbox checked={isSelected} />
<span className="flex-1 min-w-0 font-medium text-sm overflow-hidden text-ellipsis whitespace-nowrap"> <span className="flex-1 min-w-0 font-medium text-sm overflow-hidden text-ellipsis whitespace-nowrap">
{operator.name} {operator.name}
</span> </span>
</div> </div>
<span <span
className="cursor-pointer" className="cursor-pointer"
onClick={(event) => { onClick={(event) => {
event.stopPropagation(); event.stopPropagation();
handleStar(operator, toggleFavorite); handleStar(operator, toggleFavorite);
}} }}
> >
{favorites.has(operator.id) ? ( {favorites.has(operator.id) ? (
<StarFilled style={{ color: "#FFD700" }} /> <StarFilled style={{ color: "#FFD700" }} />
) : ( ) : (
<StarOutlined /> <StarOutlined />
)} )}
</span> </span>
</div> </div>
</Card> </Card>
); );
})} })}
</div> </div>
); );
interface OperatorLibraryProps { interface OperatorLibraryProps {
selectedOperators: OperatorI[]; selectedOperators: OperatorI[];
operatorList: OperatorI[]; operatorList: OperatorI[];
categoryOptions: CategoryI[]; categoryOptions: CategoryI[];
setSelectedOperators: (operators: OperatorI[]) => void; setSelectedOperators: (operators: OperatorI[]) => void;
toggleOperator: (template: OperatorI) => void; toggleOperator: (template: OperatorI) => void;
handleDragStart: ( handleDragStart: (
e: React.DragEvent, e: React.DragEvent,
item: OperatorI, item: OperatorI,
source: "library" source: "library"
) => void; ) => void;
} }
const OperatorLibrary: React.FC<OperatorLibraryProps> = ({ const OperatorLibrary: React.FC<OperatorLibraryProps> = ({
selectedOperators, selectedOperators,
operatorList, operatorList,
categoryOptions, categoryOptions,
setSelectedOperators, setSelectedOperators,
toggleOperator, toggleOperator,
handleDragStart, handleDragStart,
}) => { }) => {
const [searchTerm, setSearchTerm] = useState(""); const [searchTerm, setSearchTerm] = useState("");
const [showFavorites, setShowFavorites] = useState(false); const [showFavorites, setShowFavorites] = useState(false);
const [favorites, setFavorites] = useState<Set<string>>(new Set()); const [favorites, setFavorites] = useState<Set<string>>(new Set());
const [selectedCategory, setSelectedCategory] = useState<string>("all"); const [selectedCategory, setSelectedCategory] = useState<string>("all");
const [expandedCategories, setExpandedCategories] = useState<Set<string>>( const [expandedCategories, setExpandedCategories] = useState<Set<string>>(
new Set([]) new Set([])
); );
// 按分类分组 // 按分类分组
const groupedOperators = useMemo(() => { const groupedOperators = useMemo(() => {
const groups: { [key: string]: OperatorI[] } = {}; const groups: { [key: string]: OperatorI[] } = {};
categoryOptions.forEach((cat: any) => { categoryOptions.forEach((cat: any) => {
groups[cat.name] = { groups[cat.name] = {
...cat, ...cat,
operators: operatorList.filter((op) => op.categories?.includes(cat.id)), operators: operatorList.filter((op) => op.categories?.includes(cat.id)),
}; };
}); });
if (selectedCategory && selectedCategory !== "all") { if (selectedCategory && selectedCategory !== "all") {
Object.keys(groups).forEach((key) => { Object.keys(groups).forEach((key) => {
if (groups[key].id !== selectedCategory) { if (groups[key].id !== selectedCategory) {
delete groups[key]; delete groups[key];
} }
}); });
} }
if (searchTerm) { if (searchTerm) {
Object.keys(groups).forEach((key) => { Object.keys(groups).forEach((key) => {
groups[key].operators = groups[key].operators.filter((operator) => groups[key].operators = groups[key].operators.filter((operator) =>
operator.name.toLowerCase().includes(searchTerm.toLowerCase()) operator.name.toLowerCase().includes(searchTerm.toLowerCase())
); );
if (groups[key].operators.length === 0) { if (groups[key].operators.length === 0) {
delete groups[key]; delete groups[key];
} }
}); });
} }
if (showFavorites) { if (showFavorites) {
Object.keys(groups).forEach((key) => { Object.keys(groups).forEach((key) => {
groups[key].operators = groups[key].operators.filter((operator) => groups[key].operators = groups[key].operators.filter((operator) =>
favorites.has(operator.id) favorites.has(operator.id)
); );
if (groups[key].operators.length === 0) { if (groups[key].operators.length === 0) {
delete groups[key]; delete groups[key];
} }
}); });
} }
setExpandedCategories(new Set(Object.keys(groups))); setExpandedCategories(new Set(Object.keys(groups)));
return groups; return groups;
}, [categoryOptions, selectedCategory, searchTerm, showFavorites]); }, [categoryOptions, selectedCategory, searchTerm, showFavorites]);
// 过滤算子 // 过滤算子
const filteredOperators = useMemo(() => { const filteredOperators = useMemo(() => {
return Object.values(groupedOperators).flatMap( return Object.values(groupedOperators).flatMap(
(category) => category.operators (category) => category.operators
); );
}, [groupedOperators]); }, [groupedOperators]);
// 收藏切换 // 收藏切换
const toggleFavorite = (operatorId: string) => { const toggleFavorite = (operatorId: string) => {
const newFavorites = new Set(favorites); const newFavorites = new Set(favorites);
if (newFavorites.has(operatorId)) { if (newFavorites.has(operatorId)) {
newFavorites.delete(operatorId); newFavorites.delete(operatorId);
} else { } else {
newFavorites.add(operatorId); newFavorites.add(operatorId);
} }
setFavorites(newFavorites); setFavorites(newFavorites);
}; };
const fetchFavorite = async () => { const fetchFavorite = async () => {
const newFavorites = new Set(favorites); const newFavorites = new Set(favorites);
operatorList.forEach(item => { operatorList.forEach(item => {
item.isStar && newFavorites.add(item.id); item.isStar && newFavorites.add(item.id);
}); });
setFavorites(newFavorites); setFavorites(newFavorites);
} }
useEffect(() => { useEffect(() => {
fetchFavorite() fetchFavorite()
}, [operatorList]); }, [operatorList]);
// 全选分类算子 // 全选分类算子
const handleSelectAll = (operators: OperatorI[]) => { const handleSelectAll = (operators: OperatorI[]) => {
const newSelected = [...selectedOperators]; const newSelected = [...selectedOperators];
operators.forEach((operator) => { operators.forEach((operator) => {
if (!newSelected.some((op) => op.id === operator.id)) { if (!newSelected.some((op) => op.id === operator.id)) {
newSelected.push(operator); newSelected.push(operator);
} }
}); });
setSelectedOperators(newSelected); setSelectedOperators(newSelected);
}; };
return ( return (
<div className="w-1/4 h-full min-w-3xs flex flex-col"> <div className="w-1/4 h-full min-w-3xs flex flex-col">
<div className="pb-4 border-b border-gray-200"> <div className="pb-4 border-b border-gray-200">
<span className="flex items-center font-semibold text-base"> <span className="flex items-center font-semibold text-base">
<Layers className="w-4 h-4 mr-2" /> <Layers className="w-4 h-4 mr-2" />
({filteredOperators.length}) ({filteredOperators.length})
</span> </span>
</div> </div>
<div className="flex flex-col h-full pt-4 pr-4 overflow-hidden"> <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"> <div className="flex flex-wrap gap-2 border-b border-gray-100 pb-4">
<Input <Input
prefix={<SearchOutlined />} prefix={<SearchOutlined />}
placeholder="搜索算子名称..." placeholder="搜索算子名称..."
value={searchTerm} value={searchTerm}
allowClear allowClear
onChange={(e) => setSearchTerm(e.target.value)} onChange={(e) => setSearchTerm(e.target.value)}
/> />
<Select <Select
value={selectedCategory} value={selectedCategory}
options={[{ label: "全部分类", value: "all" }, ...categoryOptions]} options={[{ label: "全部分类", value: "all" }, ...categoryOptions]}
onChange={setSelectedCategory} onChange={setSelectedCategory}
className="flex-1" className="flex-1"
placeholder="选择分类" placeholder="选择分类"
></Select> ></Select>
<Tooltip title="只看收藏"> <Tooltip title="只看收藏">
<span <span
className="cursor-pointer" className="cursor-pointer"
onClick={() => setShowFavorites(!showFavorites)} onClick={() => setShowFavorites(!showFavorites)}
> >
{showFavorites ? ( {showFavorites ? (
<StarFilled style={{ color: "#FFD700" }} /> <StarFilled style={{ color: "#FFD700" }} />
) : ( ) : (
<StarOutlined /> <StarOutlined />
)} )}
</span> </span>
</Tooltip> </Tooltip>
</div> </div>
{/* 算子列表 */} {/* 算子列表 */}
<div className="flex-1 overflow-auto"> <div className="flex-1 overflow-auto">
{/* 分类算子 */} {/* 分类算子 */}
<Collapse <Collapse
ghost ghost
activeKey={Array.from(expandedCategories)} activeKey={Array.from(expandedCategories)}
onChange={(keys) => onChange={(keys) =>
setExpandedCategories( setExpandedCategories(
new Set(Array.isArray(keys) ? keys : [keys]) new Set(Array.isArray(keys) ? keys : [keys])
) )
} }
> >
{Object.entries(groupedOperators).map(([key, category]) => ( {Object.entries(groupedOperators).map(([key, category]) => (
<Collapse.Panel <Collapse.Panel
key={key} key={key}
header={ header={
<div className="flex items-center justify-between w-full"> <div className="flex items-center justify-between w-full">
<span className="flex items-center gap-2"> <span className="flex items-center gap-2">
<span>{category.name}</span> <span>{category.name}</span>
<Tag>{category.operators.length}</Tag> <Tag>{category.operators.length}</Tag>
</span> </span>
<Button <Button
type="link" type="link"
size="small" size="small"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
handleSelectAll(category.operators); handleSelectAll(category.operators);
}} }}
> >
</Button> </Button>
</div> </div>
} }
> >
<OperatorList <OperatorList
selectedOperators={selectedOperators} selectedOperators={selectedOperators}
operators={category.operators} operators={category.operators}
favorites={favorites} favorites={favorites}
toggleOperator={toggleOperator} toggleOperator={toggleOperator}
onDragOperator={handleDragStart} onDragOperator={handleDragStart}
toggleFavorite={toggleFavorite} toggleFavorite={toggleFavorite}
/> />
</Collapse.Panel> </Collapse.Panel>
))} ))}
</Collapse> </Collapse>
{filteredOperators.length === 0 && ( {filteredOperators.length === 0 && (
<div className="text-center py-8 text-gray-400"> <div className="text-center py-8 text-gray-400">
<SearchOutlined className="text-3xl mb-2 opacity-50" /> <SearchOutlined className="text-3xl mb-2 opacity-50" />
<div></div> <div></div>
</div> </div>
)} )}
</div> </div>
</div> </div>
</div> </div>
); );
}; };
export default OperatorLibrary; export default OperatorLibrary;

View File

@@ -1,213 +1,213 @@
import React, {useMemo, useState} from "react"; import React, {useMemo, useState} from "react";
import { Card, Input, Tag, Select, Button } from "antd"; import { Card, Input, Tag, Select, Button } from "antd";
import { DeleteOutlined } from "@ant-design/icons"; import { DeleteOutlined } from "@ant-design/icons";
import { CleansingTemplate } from "../../cleansing.model"; import { CleansingTemplate } from "../../cleansing.model";
import { Workflow } from "lucide-react"; import { Workflow } from "lucide-react";
import {CategoryI, OperatorI} from "@/pages/OperatorMarket/operator.model"; import {CategoryI, OperatorI} from "@/pages/OperatorMarket/operator.model";
interface OperatorFlowProps { interface OperatorFlowProps {
selectedOperators: OperatorI[]; selectedOperators: OperatorI[];
configOperator: OperatorI | null; configOperator: OperatorI | null;
templates: CleansingTemplate[]; templates: CleansingTemplate[];
currentTemplate: CleansingTemplate | null; currentTemplate: CleansingTemplate | null;
categoryOptions: []; categoryOptions: [];
setCurrentTemplate: (template: CleansingTemplate | null) => void; setCurrentTemplate: (template: CleansingTemplate | null) => void;
removeOperator: (id: string) => void; removeOperator: (id: string) => void;
setSelectedOperators: (operators: OperatorI[]) => void; setSelectedOperators: (operators: OperatorI[]) => void;
setConfigOperator: (operator: OperatorI | null) => void; setConfigOperator: (operator: OperatorI | null) => void;
handleDragStart: ( handleDragStart: (
e: React.DragEvent, e: React.DragEvent,
operator: OperatorI, operator: OperatorI,
source: "sort" source: "sort"
) => void; ) => void;
handleItemDragOver: (e: React.DragEvent, itemId: string) => void; handleItemDragOver: (e: React.DragEvent, itemId: string) => void;
handleItemDragLeave: (e: React.DragEvent) => void; handleItemDragLeave: (e: React.DragEvent) => void;
handleItemDrop: (e: React.DragEvent, index: number) => void; handleItemDrop: (e: React.DragEvent, index: number) => void;
handleContainerDragOver: (e: React.DragEvent) => void; handleContainerDragOver: (e: React.DragEvent) => void;
handleContainerDragLeave: (e: React.DragEvent) => void; handleContainerDragLeave: (e: React.DragEvent) => void;
handleDragEnd: (e: React.DragEvent) => void; handleDragEnd: (e: React.DragEvent) => void;
handleDropToContainer: (e: React.DragEvent) => void; handleDropToContainer: (e: React.DragEvent) => void;
} }
const OperatorFlow: React.FC<OperatorFlowProps> = ({ const OperatorFlow: React.FC<OperatorFlowProps> = ({
selectedOperators, selectedOperators,
configOperator, configOperator,
templates, templates,
currentTemplate, currentTemplate,
categoryOptions, categoryOptions,
setSelectedOperators, setSelectedOperators,
setConfigOperator, setConfigOperator,
removeOperator, removeOperator,
setCurrentTemplate, setCurrentTemplate,
handleDragStart, handleDragStart,
handleItemDragLeave, handleItemDragLeave,
handleItemDragOver, handleItemDragOver,
handleItemDrop, handleItemDrop,
handleContainerDragLeave, handleContainerDragLeave,
handleDropToContainer, handleDropToContainer,
handleDragEnd, handleDragEnd,
}) => { }) => {
const [editingIndex, setEditingIndex] = useState<string | null>(null); const [editingIndex, setEditingIndex] = useState<string | null>(null);
const categoryMap = useMemo(() => { const categoryMap = useMemo(() => {
const map: { [key: string]: CategoryI } = {}; const map: { [key: string]: CategoryI } = {};
categoryOptions.forEach((cat: any) => { categoryOptions.forEach((cat: any) => {
map[cat.id] = { map[cat.id] = {
...cat, ...cat,
}; };
}); });
return map; return map;
}, [categoryOptions]); }, [categoryOptions]);
// 添加编号修改处理函数 // 添加编号修改处理函数
const handleIndexChange = (operatorId: string, newIndex: string) => { const handleIndexChange = (operatorId: string, newIndex: string) => {
const index = Number.parseInt(newIndex); const index = Number.parseInt(newIndex);
if (isNaN(index) || index < 1 || index > selectedOperators.length) { if (isNaN(index) || index < 1 || index > selectedOperators.length) {
return; // 无效输入,不处理 return; // 无效输入,不处理
} }
const currentIndex = selectedOperators.findIndex( const currentIndex = selectedOperators.findIndex(
(op) => op.id === operatorId (op) => op.id === operatorId
); );
if (currentIndex === -1) return; if (currentIndex === -1) return;
const targetIndex = index - 1; // 转换为0基索引 const targetIndex = index - 1; // 转换为0基索引
if (currentIndex === targetIndex) return; // 位置没有变化 if (currentIndex === targetIndex) return; // 位置没有变化
const newOperators = [...selectedOperators]; const newOperators = [...selectedOperators];
const [movedOperator] = newOperators.splice(currentIndex, 1); const [movedOperator] = newOperators.splice(currentIndex, 1);
newOperators.splice(targetIndex, 0, movedOperator); newOperators.splice(targetIndex, 0, movedOperator);
setSelectedOperators(newOperators); setSelectedOperators(newOperators);
setEditingIndex(null); setEditingIndex(null);
}; };
return ( return (
<div className="w-1/2 h-full min-w-xs flex-1 flex flex-col border-x border-gray-200"> <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="px-4 pb-2 border-b border-gray-200">
<div className="flex flex-wrap gap-2 justify-between items-start"> <div className="flex flex-wrap gap-2 justify-between items-start">
<span className="font-semibold text-base flex items-center gap-2"> <span className="font-semibold text-base flex items-center gap-2">
<Workflow className="w-5 h-5" /> <Workflow className="w-5 h-5" />
({selectedOperators.length}){" "} ({selectedOperators.length}){" "}
<Button <Button
type="link" type="link"
size="small" size="small"
onClick={() => { onClick={() => {
setConfigOperator(null); setConfigOperator(null);
setSelectedOperators([]); setSelectedOperators([]);
}} }}
disabled={selectedOperators.length === 0} disabled={selectedOperators.length === 0}
> >
</Button> </Button>
</span> </span>
<Select <Select
placeholder="选择模板" placeholder="选择模板"
className="min-w-64" className="min-w-64"
options={templates} options={templates}
value={currentTemplate?.value} value={currentTemplate?.value}
onChange={(value) => onChange={(value) =>
setCurrentTemplate( setCurrentTemplate(
templates.find((t) => t.value === value) || null templates.find((t) => t.value === value) || null
) )
} }
></Select> ></Select>
</div> </div>
</div> </div>
{/* 编排区域 */} {/* 编排区域 */}
<div <div
className="flex-overflow-auto p-4 gap-2" className="flex-overflow-auto p-4 gap-2"
onDragOver={(e) => e.preventDefault()} onDragOver={(e) => e.preventDefault()}
onDragLeave={handleContainerDragLeave} onDragLeave={handleContainerDragLeave}
onDrop={handleDropToContainer} onDrop={handleDropToContainer}
> >
{selectedOperators.map((operator, index) => ( {selectedOperators.map((operator, index) => (
<Card <Card
size="small" size="small"
key={operator.id} key={operator.id}
style={ style={
configOperator?.id === operator.id configOperator?.id === operator.id
? { borderColor: "#1677ff" } ? { borderColor: "#1677ff" }
: {} : {}
} }
hoverable hoverable
draggable draggable
onDragStart={(e) => handleDragStart(e, operator, "sort")} onDragStart={(e) => handleDragStart(e, operator, "sort")}
onDragEnd={handleDragEnd} onDragEnd={handleDragEnd}
onDragOver={(e) => handleItemDragOver(e, operator.id)} onDragOver={(e) => handleItemDragOver(e, operator.id)}
onDragLeave={handleItemDragLeave} onDragLeave={handleItemDragLeave}
onDrop={(e) => handleItemDrop(e, index)} onDrop={(e) => handleItemDrop(e, index)}
onClick={() => setConfigOperator(operator)} onClick={() => setConfigOperator(operator)}
> >
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
{/* 可编辑编号 */} {/* 可编辑编号 */}
<span></span> <span></span>
{editingIndex === operator.id ? ( {editingIndex === operator.id ? (
<Input <Input
type="number" type="number"
min={1} min={1}
max={selectedOperators.length} max={selectedOperators.length}
defaultValue={index + 1} defaultValue={index + 1}
className="w-10 h-6 text-xs text-center" className="w-10 h-6 text-xs text-center"
style={{ width: 60 }} style={{ width: 60 }}
autoFocus autoFocus
onBlur={(e) => handleIndexChange(operator.id, e.target.value)} onBlur={(e) => handleIndexChange(operator.id, e.target.value)}
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.key === "Enter") if (e.key === "Enter")
handleIndexChange( handleIndexChange(
operator.id, operator.id,
(e.target as HTMLInputElement).value (e.target as HTMLInputElement).value
); );
else if (e.key === "Escape") setEditingIndex(null); else if (e.key === "Escape") setEditingIndex(null);
}} }}
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
/> />
) : ( ) : (
<Tag <Tag
color="default" color="default"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
setEditingIndex(operator.id); setEditingIndex(operator.id);
}} }}
> >
{index + 1} {index + 1}
</Tag> </Tag>
)} )}
{/* 算子图标和名称 */} {/* 算子图标和名称 */}
<div className="flex items-center gap-2 min-w-0 flex-1"> <div className="flex items-center gap-2 min-w-0 flex-1">
<span className="font-medium text-sm truncate"> <span className="font-medium text-sm truncate">
{operator.name} {operator.name}
</span> </span>
</div> </div>
{operator?.categories?.map((categoryId) => { {operator?.categories?.map((categoryId) => {
return <Tag color="default">{categoryMap[categoryId].name}</Tag> return <Tag color="default">{categoryMap[categoryId].name}</Tag>
})} })}
{/* 操作按钮 */} {/* 操作按钮 */}
<span <span
className="cursor-pointer text-red-500" className="cursor-pointer text-red-500"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
removeOperator(operator.id); removeOperator(operator.id);
}} }}
> >
<DeleteOutlined /> <DeleteOutlined />
</span> </span>
</div> </div>
</Card> </Card>
))} ))}
{selectedOperators.length === 0 && ( {selectedOperators.length === 0 && (
<div className="text-center py-16 text-gray-400 border-2 border-dashed border-gray-100 rounded-lg"> <div className="text-center py-16 text-gray-400 border-2 border-dashed border-gray-100 rounded-lg">
<Workflow className="w-full h-10 mb-4 opacity-50" /> <Workflow className="w-full h-10 mb-4 opacity-50" />
<div className="text-lg font-medium mb-2"></div> <div className="text-lg font-medium mb-2"></div>
<div className="text-sm"> <div className="text-sm">
</div> </div>
</div> </div>
)} )}
</div> </div>
</div> </div>
); );
}; };
export default OperatorFlow; export default OperatorFlow;

View File

@@ -1,245 +1,245 @@
import React from "react"; import React from "react";
import { import {
Input, Input,
Select, Select,
Radio, Radio,
Checkbox, Checkbox,
Form, Form,
InputNumber, InputNumber,
Slider, Slider,
Space, Space,
} from "antd"; } from "antd";
import { ConfigI, OperatorI } from "@/pages/OperatorMarket/operator.model"; import { ConfigI, OperatorI } from "@/pages/OperatorMarket/operator.model";
interface ParamConfigProps { interface ParamConfigProps {
operator: OperatorI; operator: OperatorI;
paramKey: string; paramKey: string;
param: ConfigI; param: ConfigI;
onParamChange?: (operatorId: string, paramKey: string, value: any) => void; onParamChange?: (operatorId: string, paramKey: string, value: any) => void;
} }
const ParamConfig: React.FC<ParamConfigProps> = ({ const ParamConfig: React.FC<ParamConfigProps> = ({
operator, operator,
paramKey, paramKey,
param, param,
onParamChange, onParamChange,
}) => { }) => {
if (!param) return null; if (!param) return null;
let defaultVal: any = param.defaultVal; let defaultVal: any = param.defaultVal;
if (param.type === "range") { if (param.type === "range") {
defaultVal = Array.isArray(param.defaultVal) defaultVal = Array.isArray(param.defaultVal)
? param.defaultVal ? param.defaultVal
: [ : [
param?.properties?.[0]?.defaultVal, param?.properties?.[0]?.defaultVal,
param?.properties?.[1]?.defaultVal, param?.properties?.[1]?.defaultVal,
]; ];
} }
const [value, setValue] = React.useState(param.value || defaultVal); const [value, setValue] = React.useState(param.value || defaultVal);
const updateValue = (newValue: any) => { const updateValue = (newValue: any) => {
setValue(newValue); setValue(newValue);
return onParamChange && onParamChange(operator.id, paramKey, newValue); return onParamChange && onParamChange(operator.id, paramKey, newValue);
}; };
switch (param.type) { switch (param.type) {
case "input": case "input":
return ( return (
<Form.Item <Form.Item
label={param.name} label={param.name}
tooltip={param.description} tooltip={param.description}
key={paramKey} key={paramKey}
> >
<Input <Input
value={value} value={value}
onChange={(e) => updateValue(e.target.value)} onChange={(e) => updateValue(e.target.value)}
placeholder={`请输入${param.name}`} placeholder={`请输入${param.name}`}
className="w-full" className="w-full"
/> />
</Form.Item> </Form.Item>
); );
case "select": case "select":
return ( return (
<Form.Item <Form.Item
label={param.name} label={param.name}
tooltip={param.description} tooltip={param.description}
key={paramKey} key={paramKey}
> >
<Select <Select
value={value} value={value}
onChange={updateValue} onChange={updateValue}
options={(param.options || []).map((option: any) => options={(param.options || []).map((option: any) =>
typeof option === "string" typeof option === "string"
? { label: option, value: option } ? { label: option, value: option }
: option : option
)} )}
placeholder={`请选择${param.name}`} placeholder={`请选择${param.name}`}
className="w-full" className="w-full"
/> />
</Form.Item> </Form.Item>
); );
case "radio": case "radio":
return ( return (
<Form.Item <Form.Item
label={param.name} label={param.name}
tooltip={param.description} tooltip={param.description}
key={paramKey} key={paramKey}
> >
<Radio.Group <Radio.Group
value={value} value={value}
onChange={(e) => updateValue(e.target.value)} onChange={(e) => updateValue(e.target.value)}
> >
{(param.options || []).map((option: any) => ( {(param.options || []).map((option: any) => (
<Radio <Radio
key={typeof option === "string" ? option : option.value} key={typeof option === "string" ? option : option.value}
value={typeof option === "string" ? option : option.value} value={typeof option === "string" ? option : option.value}
> >
{typeof option === "string" ? option : option.label} {typeof option === "string" ? option : option.label}
</Radio> </Radio>
))} ))}
</Radio.Group> </Radio.Group>
</Form.Item> </Form.Item>
); );
case "checkbox": case "checkbox":
return ( return (
<Form.Item <Form.Item
label={param.name} label={param.name}
tooltip={param.description} tooltip={param.description}
key={paramKey} key={paramKey}
> >
<Checkbox.Group <Checkbox.Group
value={value} value={value}
onChange={updateValue} onChange={updateValue}
options={param.options || []} options={param.options || []}
/> />
</Form.Item> </Form.Item>
); );
case "slider": case "slider":
return ( return (
<Form.Item <Form.Item
label={param.name} label={param.name}
tooltip={param.description} tooltip={param.description}
key={paramKey} key={paramKey}
> >
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<Slider <Slider
value={value} value={value}
onChange={updateValue} onChange={updateValue}
tooltip={{ open: true }} tooltip={{ open: true }}
marks={{ marks={{
[param.min || 0]: `${param.min || 0}`, [param.min || 0]: `${param.min || 0}`,
[param.min + (param.max - param.min) / 2]: `${ [param.min + (param.max - param.min) / 2]: `${
(param.min + param.max) / 2 (param.min + param.max) / 2
}`, }`,
[param.max || 100]: `${param.max || 100}`, [param.max || 100]: `${param.max || 100}`,
}} }}
min={param.min || 0} min={param.min || 0}
max={param.max || 100} max={param.max || 100}
step={param.step || 1} step={param.step || 1}
className="flex-1" className="flex-1"
/> />
<InputNumber <InputNumber
min={param.min || 0} min={param.min || 0}
max={param.max || 100} max={param.max || 100}
step={param.step || 1} step={param.step || 1}
value={value} value={value}
onChange={updateValue} onChange={updateValue}
style={{ width: 80 }} style={{ width: 80 }}
/> />
</div> </div>
</Form.Item> </Form.Item>
); );
case "range": { case "range": {
const min = param.min || param?.properties?.[0]?.min || 0; const min = param.min || param?.properties?.[0]?.min || 0;
const max = param.max || param?.properties?.[0]?.max || 1; const max = param.max || param?.properties?.[0]?.max || 1;
const step = param.step || param?.properties?.[0]?.step || 0.1; const step = param.step || param?.properties?.[0]?.step || 0.1;
return ( return (
<Form.Item <Form.Item
label={param.name} label={param.name}
tooltip={param.description} tooltip={param.description}
key={paramKey} key={paramKey}
> >
<Slider <Slider
value={Array.isArray(value) ? value : [value, value]} value={Array.isArray(value) ? value : [value, value]}
onChange={(val) => onChange={(val) =>
updateValue(Array.isArray(val) ? val : [val, val]) updateValue(Array.isArray(val) ? val : [val, val])
} }
range range
min={min} min={min}
max={max } max={max }
step={step} step={step}
className="w-full" className="w-full"
/> />
<Space> <Space>
<InputNumber <InputNumber
min={min} min={min}
max={max} max={max}
value={value[0]} value={value[0]}
onChange={(val1) => updateValue([val1, value[1]])} onChange={(val1) => updateValue([val1, value[1]])}
changeOnWheel changeOnWheel
/> />
~ ~
<InputNumber <InputNumber
min={min} min={min}
max={max} max={max}
value={value[1]} value={value[1]}
onChange={(val2) => updateValue([value[0], val2])} onChange={(val2) => updateValue([value[0], val2])}
changeOnWheel changeOnWheel
/> />
</Space> </Space>
</Form.Item> </Form.Item>
); );
} }
case "inputNumber": case "inputNumber":
return ( return (
<Form.Item <Form.Item
label={param.name} label={param.name}
tooltip={param.description} tooltip={param.description}
key={paramKey} key={paramKey}
> >
<InputNumber <InputNumber
value={value} value={value}
onChange={(val) => updateValue(val)} onChange={(val) => updateValue(val)}
placeholder={`请输入${param.name}`} placeholder={`请输入${param.name}`}
className="w-full" className="w-full"
min={param.min} min={param.min}
max={param.max} max={param.max}
step={param.step || 1} step={param.step || 1}
/> />
</Form.Item> </Form.Item>
); );
case "switch": case "switch":
return ( return (
<Form.Item <Form.Item
label={param.name} label={param.name}
tooltip={param.description} tooltip={param.description}
key={paramKey} key={paramKey}
> >
<Checkbox <Checkbox
checked={value as boolean} checked={value as boolean}
onChange={(e) => updateValue(e.target.checked)} onChange={(e) => updateValue(e.target.checked)}
> >
{param.name} {param.name}
</Checkbox> </Checkbox>
</Form.Item> </Form.Item>
); );
case "multiple": case "multiple":
return ( return (
<div className="pl-4 border-l border-gray-300"> <div className="pl-4 border-l border-gray-300">
{param.properties.map((subParam) => ( {param.properties.map((subParam) => (
<ParamConfig <ParamConfig
key={subParam.key} key={subParam.key}
operator={operator} operator={operator}
paramKey={subParam.key} paramKey={subParam.key}
param={subParam} param={subParam}
onParamChange={onParamChange} onParamChange={onParamChange}
/> />
))} ))}
</div> </div>
); );
default: default:
return null; return null;
} }
}; };
export default ParamConfig; export default ParamConfig;

View File

@@ -1,87 +1,87 @@
import { useDragOperators } from "./useDragOperators"; import { useDragOperators } from "./useDragOperators";
import { useOperatorOperations } from "./useOperatorOperations"; import { useOperatorOperations } from "./useOperatorOperations";
import OperatorConfig from "../components/OperatorConfig"; import OperatorConfig from "../components/OperatorConfig";
import OperatorLibrary from "../components/OperatorLibrary"; import OperatorLibrary from "../components/OperatorLibrary";
import OperatorOrchestration from "../components/OperatorOrchestration"; import OperatorOrchestration from "../components/OperatorOrchestration";
export function useCreateStepTwo() { export function useCreateStepTwo() {
const { const {
operators, operators,
selectedOperators, selectedOperators,
templates, templates,
currentTemplate, currentTemplate,
configOperator, configOperator,
currentStep, currentStep,
categoryOptions, categoryOptions,
handlePrev, handlePrev,
handleNext, handleNext,
setCurrentTemplate, setCurrentTemplate,
setConfigOperator, setConfigOperator,
setSelectedOperators, setSelectedOperators,
handleConfigChange, handleConfigChange,
toggleOperator, toggleOperator,
removeOperator, removeOperator,
} = useOperatorOperations(); } = useOperatorOperations();
const { const {
handleDragStart, handleDragStart,
handleDragEnd, handleDragEnd,
handleContainerDragOver, handleContainerDragOver,
handleContainerDragLeave, handleContainerDragLeave,
handleItemDragOver, handleItemDragOver,
handleItemDragLeave, handleItemDragLeave,
handleItemDrop, handleItemDrop,
handleDropToContainer, handleDropToContainer,
} = useDragOperators({ } = useDragOperators({
operators: selectedOperators, operators: selectedOperators,
setOperators: setSelectedOperators, setOperators: setSelectedOperators,
}); });
const renderStepTwo = ( const renderStepTwo = (
<div className="flex w-full h-full"> <div className="flex w-full h-full">
{/* 左侧算子库 */} {/* 左侧算子库 */}
<OperatorLibrary <OperatorLibrary
categoryOptions={categoryOptions} categoryOptions={categoryOptions}
selectedOperators={selectedOperators} selectedOperators={selectedOperators}
operatorList={operators} operatorList={operators}
setSelectedOperators={setSelectedOperators} setSelectedOperators={setSelectedOperators}
toggleOperator={toggleOperator} toggleOperator={toggleOperator}
handleDragStart={handleDragStart} handleDragStart={handleDragStart}
/> />
{/* 中间算子编排区域 */} {/* 中间算子编排区域 */}
<OperatorOrchestration <OperatorOrchestration
selectedOperators={selectedOperators} selectedOperators={selectedOperators}
configOperator={configOperator} configOperator={configOperator}
templates={templates} templates={templates}
currentTemplate={currentTemplate} currentTemplate={currentTemplate}
categoryOptions={categoryOptions} categoryOptions={categoryOptions}
setSelectedOperators={setSelectedOperators} setSelectedOperators={setSelectedOperators}
setConfigOperator={setConfigOperator} setConfigOperator={setConfigOperator}
setCurrentTemplate={setCurrentTemplate} setCurrentTemplate={setCurrentTemplate}
removeOperator={removeOperator} removeOperator={removeOperator}
handleDragStart={handleDragStart} handleDragStart={handleDragStart}
handleContainerDragLeave={handleContainerDragLeave} handleContainerDragLeave={handleContainerDragLeave}
handleContainerDragOver={handleContainerDragOver} handleContainerDragOver={handleContainerDragOver}
handleItemDragOver={handleItemDragOver} handleItemDragOver={handleItemDragOver}
handleItemDragLeave={handleItemDragLeave} handleItemDragLeave={handleItemDragLeave}
handleItemDrop={handleItemDrop} handleItemDrop={handleItemDrop}
handleDropToContainer={handleDropToContainer} handleDropToContainer={handleDropToContainer}
handleDragEnd={handleDragEnd} handleDragEnd={handleDragEnd}
/> />
{/* 右侧参数配置面板 */} {/* 右侧参数配置面板 */}
<OperatorConfig <OperatorConfig
selectedOp={configOperator} selectedOp={configOperator}
handleConfigChange={handleConfigChange} handleConfigChange={handleConfigChange}
/> />
</div> </div>
); );
return { return {
renderStepTwo, renderStepTwo,
selectedOperators, selectedOperators,
currentStep, currentStep,
handlePrev, handlePrev,
handleNext, handleNext,
}; };
} }

View File

@@ -1,158 +1,158 @@
import { OperatorI } from "@/pages/OperatorMarket/operator.model"; import { OperatorI } from "@/pages/OperatorMarket/operator.model";
import React, { useState } from "react"; import React, { useState } from "react";
export function useDragOperators({ export function useDragOperators({
operators, operators,
setOperators, setOperators,
}: { }: {
operators: OperatorI[]; operators: OperatorI[];
setOperators: (operators: OperatorI[]) => void; setOperators: (operators: OperatorI[]) => void;
}) { }) {
const [draggingItem, setDraggingItem] = useState<OperatorI | null>(null); const [draggingItem, setDraggingItem] = useState<OperatorI | null>(null);
const [draggingSource, setDraggingSource] = useState< const [draggingSource, setDraggingSource] = useState<
"library" | "sort" | null "library" | "sort" | null
>(null); >(null);
const [insertPosition, setInsertPosition] = useState< const [insertPosition, setInsertPosition] = useState<
"above" | "below" | null "above" | "below" | null
>(null); >(null);
// 处理拖拽开始 // 处理拖拽开始
const handleDragStart = ( const handleDragStart = (
e: React.DragEvent, e: React.DragEvent,
item: OperatorI, item: OperatorI,
source: "library" | "sort" source: "library" | "sort"
) => { ) => {
setDraggingItem({ setDraggingItem({
...item, ...item,
originalId: item.id, originalId: item.id,
}); });
setDraggingSource(source); setDraggingSource(source);
e.dataTransfer.effectAllowed = "move"; e.dataTransfer.effectAllowed = "move";
}; };
// 处理拖拽结束 // 处理拖拽结束
const handleDragEnd = () => { const handleDragEnd = () => {
setDraggingItem(null); setDraggingItem(null);
setInsertPosition(null); setInsertPosition(null);
}; };
// 处理容器拖拽经过 // 处理容器拖拽经过
const handleContainerDragOver = (e: React.DragEvent) => { const handleContainerDragOver = (e: React.DragEvent) => {
e.preventDefault(); e.preventDefault();
}; };
// 处理容器拖拽离开 // 处理容器拖拽离开
const handleContainerDragLeave = (e: React.DragEvent) => { const handleContainerDragLeave = (e: React.DragEvent) => {
if (!e.currentTarget.contains(e.relatedTarget)) { if (!e.currentTarget.contains(e.relatedTarget)) {
setInsertPosition(null); setInsertPosition(null);
} }
}; };
// 处理项目拖拽经过 // 处理项目拖拽经过
const handleItemDragOver = (e: React.DragEvent) => { const handleItemDragOver = (e: React.DragEvent) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
const rect = e.currentTarget.getBoundingClientRect(); const rect = e.currentTarget.getBoundingClientRect();
const mouseY = e.clientY; const mouseY = e.clientY;
const elementMiddle = rect.top + rect.height / 2; const elementMiddle = rect.top + rect.height / 2;
// 判断鼠标在元素的上半部分还是下半部分 // 判断鼠标在元素的上半部分还是下半部分
const newPosition = mouseY < elementMiddle ? "above" : "below"; const newPosition = mouseY < elementMiddle ? "above" : "below";
setInsertPosition(newPosition); setInsertPosition(newPosition);
}; };
// 处理项目拖拽离开 // 处理项目拖拽离开
const handleItemDragLeave = (e: React.DragEvent) => { const handleItemDragLeave = (e: React.DragEvent) => {
if (!e.currentTarget.contains(e.relatedTarget)) { if (!e.currentTarget.contains(e.relatedTarget)) {
setInsertPosition(null); setInsertPosition(null);
} }
}; };
// 处理放置到空白区域 // 处理放置到空白区域
const handleDropToContainer = (e: React.DragEvent) => { const handleDropToContainer = (e: React.DragEvent) => {
e.preventDefault(); e.preventDefault();
if (!draggingItem) return; if (!draggingItem) return;
// 如果是从算子库拖拽过来的 // 如果是从算子库拖拽过来的
if (draggingSource === "library") { if (draggingSource === "library") {
// 检查是否已存在 // 检查是否已存在
const exists = operators.some((item) => item.id === draggingItem.id); const exists = operators.some((item) => item.id === draggingItem.id);
if (!exists) { if (!exists) {
setOperators([...operators, draggingItem]); setOperators([...operators, draggingItem]);
} }
} }
resetDragState(); resetDragState();
}; };
// 处理放置到特定位置 // 处理放置到特定位置
const handleItemDrop = (e: React.DragEvent, targetIndex: number) => { const handleItemDrop = (e: React.DragEvent, targetIndex: number) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
if (!draggingItem) return; if (!draggingItem) return;
// 从左侧拖拽到右侧的精确插入 // 从左侧拖拽到右侧的精确插入
if (draggingSource === "library") { if (draggingSource === "library") {
if (targetIndex !== -1) { if (targetIndex !== -1) {
const insertIndex = const insertIndex =
insertPosition === "above" ? targetIndex : targetIndex + 1; insertPosition === "above" ? targetIndex : targetIndex + 1;
// 检查是否已存在 // 检查是否已存在
const exists = operators.some((item) => item.id === draggingItem.id); const exists = operators.some((item) => item.id === draggingItem.id);
if (!exists) { if (!exists) {
const newRightItems = [...operators]; const newRightItems = [...operators];
newRightItems.splice(insertIndex, 0, draggingItem); newRightItems.splice(insertIndex, 0, draggingItem);
setOperators(newRightItems); setOperators(newRightItems);
} }
} }
} }
// 右侧容器内的重新排序 // 右侧容器内的重新排序
else if (draggingSource === "sort") { else if (draggingSource === "sort") {
const draggedIndex = operators.findIndex( const draggedIndex = operators.findIndex(
(item) => item.id === draggingItem.id (item) => item.id === draggingItem.id
); );
if ( if (
draggedIndex !== -1 && draggedIndex !== -1 &&
targetIndex !== -1 && targetIndex !== -1 &&
draggedIndex !== targetIndex draggedIndex !== targetIndex
) { ) {
const newItems = [...operators]; const newItems = [...operators];
const [draggedItem] = newItems.splice(draggedIndex, 1); const [draggedItem] = newItems.splice(draggedIndex, 1);
// 计算正确的插入位置 // 计算正确的插入位置
let insertIndex = let insertIndex =
insertPosition === "above" ? targetIndex : targetIndex + 1; insertPosition === "above" ? targetIndex : targetIndex + 1;
if (draggedIndex < insertIndex) { if (draggedIndex < insertIndex) {
insertIndex--; // 调整插入位置,因为已经移除了原元素 insertIndex--; // 调整插入位置,因为已经移除了原元素
} }
newItems.splice(insertIndex, 0, draggedItem); newItems.splice(insertIndex, 0, draggedItem);
setOperators(newItems); setOperators(newItems);
} }
} }
resetDragState(); resetDragState();
}; };
// 重置拖拽状态 // 重置拖拽状态
const resetDragState = () => { const resetDragState = () => {
setDraggingItem(null); setDraggingItem(null);
setInsertPosition(null); setInsertPosition(null);
}; };
return { return {
handleDragStart, handleDragStart,
handleDragEnd, handleDragEnd,
handleContainerDragOver, handleContainerDragOver,
handleContainerDragLeave, handleContainerDragLeave,
handleItemDragOver, handleItemDragOver,
handleItemDragLeave, handleItemDragLeave,
handleItemDrop, handleItemDrop,
handleDropToContainer, handleDropToContainer,
}; };
} }

View File

@@ -1,169 +1,169 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { OperatorI } from "@/pages/OperatorMarket/operator.model"; import { OperatorI } from "@/pages/OperatorMarket/operator.model";
import { CleansingTemplate } from "../../cleansing.model"; import { CleansingTemplate } from "../../cleansing.model";
import {queryCleaningTemplateByIdUsingGet, queryCleaningTemplatesUsingGet} from "../../cleansing.api"; import {queryCleaningTemplateByIdUsingGet, queryCleaningTemplatesUsingGet} from "../../cleansing.api";
import { import {
queryCategoryTreeUsingGet, queryCategoryTreeUsingGet,
queryOperatorsUsingPost, queryOperatorsUsingPost,
} from "@/pages/OperatorMarket/operator.api"; } from "@/pages/OperatorMarket/operator.api";
import {useParams} from "react-router"; import {useParams} from "react-router";
export function useOperatorOperations() { export function useOperatorOperations() {
const { id = "" } = useParams(); const { id = "" } = useParams();
const [currentStep, setCurrentStep] = useState(1); const [currentStep, setCurrentStep] = useState(1);
const [operators, setOperators] = useState<OperatorI[]>([]); const [operators, setOperators] = useState<OperatorI[]>([]);
const [selectedOperators, setSelectedOperators] = useState<OperatorI[]>([]); const [selectedOperators, setSelectedOperators] = useState<OperatorI[]>([]);
const [configOperator, setConfigOperator] = useState<OperatorI | null>(null); const [configOperator, setConfigOperator] = useState<OperatorI | null>(null);
const [templates, setTemplates] = useState<CleansingTemplate[]>([]); const [templates, setTemplates] = useState<CleansingTemplate[]>([]);
const [currentTemplate, setCurrentTemplate] = const [currentTemplate, setCurrentTemplate] =
useState<CleansingTemplate | null>(null); useState<CleansingTemplate | null>(null);
// 将后端返回的算子数据映射为前端需要的格式 // 将后端返回的算子数据映射为前端需要的格式
const mapOperator = (op: OperatorI) => { const mapOperator = (op: OperatorI) => {
const configs = const configs =
op.settings op.settings
? JSON.parse(op.settings) ? JSON.parse(op.settings)
: {}; : {};
const defaultParams: Record<string, string> = {}; const defaultParams: Record<string, string> = {};
Object.keys(configs).forEach((key) => { Object.keys(configs).forEach((key) => {
const { value } = configs[key]; const { value } = configs[key];
defaultParams[key] = value; defaultParams[key] = value;
}); });
return { return {
...op, ...op,
defaultParams, defaultParams,
configs, configs,
}; };
}; };
const [categoryOptions, setCategoryOptions] = useState([]); const [categoryOptions, setCategoryOptions] = useState([]);
const initOperators = async () => { const initOperators = async () => {
const [categoryRes, operatorRes] = await Promise.all([ const [categoryRes, operatorRes] = await Promise.all([
queryCategoryTreeUsingGet(), queryCategoryTreeUsingGet(),
queryOperatorsUsingPost({ page: 0, size: 1000 }), queryOperatorsUsingPost({ page: 0, size: 1000 }),
]); ]);
const operators = operatorRes.data.content.map(mapOperator); const operators = operatorRes.data.content.map(mapOperator);
setOperators(operators || []); setOperators(operators || []);
const options = categoryRes.data.content.reduce((acc: any[], item: any) => { const options = categoryRes.data.content.reduce((acc: any[], item: any) => {
const cats = item.categories.map((cat) => ({ const cats = item.categories.map((cat) => ({
...cat, ...cat,
type: item.name, type: item.name,
label: cat.name, label: cat.name,
value: cat.id, value: cat.id,
icon: cat.icon, icon: cat.icon,
operators: operators.filter((op) => op[item.name] === cat.name), operators: operators.filter((op) => op[item.name] === cat.name),
})); }));
acc.push(...cats); acc.push(...cats);
return acc; return acc;
}, [] as { id: string; name: string; icon: React.ReactNode }[]); }, [] as { id: string; name: string; icon: React.ReactNode }[]);
setCategoryOptions(options); setCategoryOptions(options);
}; };
const initTemplates = async () => { const initTemplates = async () => {
if (id) { if (id) {
const { data } = await queryCleaningTemplateByIdUsingGet(id); const { data } = await queryCleaningTemplateByIdUsingGet(id);
const template = { const template = {
...data, ...data,
label: data.name, label: data.name,
value: data.id, value: data.id,
} }
setTemplates([template]) setTemplates([template])
setCurrentTemplate(template) setCurrentTemplate(template)
} else { } else {
const { data } = await queryCleaningTemplatesUsingGet(); const { data } = await queryCleaningTemplatesUsingGet();
const newTemplates = const newTemplates =
data.content?.map?.((item) => ({ data.content?.map?.((item) => ({
...item, ...item,
label: item.name, label: item.name,
value: item.id, value: item.id,
})) || []; })) || [];
setTemplates(newTemplates); setTemplates(newTemplates);
setCurrentTemplate(newTemplates?.[0]) setCurrentTemplate(newTemplates?.[0])
} }
}; };
useEffect(() => { useEffect(() => {
setSelectedOperators(currentTemplate?.instance?.map(mapOperator) || []); setSelectedOperators(currentTemplate?.instance?.map(mapOperator) || []);
}, [currentTemplate]); }, [currentTemplate]);
useEffect(() => { useEffect(() => {
initTemplates(); initTemplates();
initOperators(); initOperators();
}, []); }, []);
const toggleOperator = (operator: OperatorI) => { const toggleOperator = (operator: OperatorI) => {
const exist = selectedOperators.find((op) => op.id === operator.id); const exist = selectedOperators.find((op) => op.id === operator.id);
if (exist) { if (exist) {
setSelectedOperators( setSelectedOperators(
selectedOperators.filter((op) => op.id !== operator.id) selectedOperators.filter((op) => op.id !== operator.id)
); );
} else { } else {
setSelectedOperators([...selectedOperators, { ...operator }]); setSelectedOperators([...selectedOperators, { ...operator }]);
} }
}; };
// 删除算子 // 删除算子
const removeOperator = (id: string) => { const removeOperator = (id: string) => {
setSelectedOperators(selectedOperators.filter((op) => op.id !== id)); setSelectedOperators(selectedOperators.filter((op) => op.id !== id));
if (configOperator?.id === id) setConfigOperator(null); if (configOperator?.id === id) setConfigOperator(null);
}; };
// 配置算子参数变化 // 配置算子参数变化
const handleConfigChange = ( const handleConfigChange = (
operatorId: string, operatorId: string,
paramKey: string, paramKey: string,
value: any value: any
) => { ) => {
setSelectedOperators((prev) => setSelectedOperators((prev) =>
prev.map((op) => prev.map((op) =>
op.id === operatorId op.id === operatorId
? { ? {
...op, ...op,
overrides: { overrides: {
...(op?.overrides || op?.defaultParams), ...(op?.overrides || op?.defaultParams),
[paramKey]: value, [paramKey]: value,
}, },
} }
: op : op
) )
); );
}; };
const handleNext = () => { const handleNext = () => {
if (currentStep < 2) { if (currentStep < 2) {
setCurrentStep(currentStep + 1); setCurrentStep(currentStep + 1);
} }
}; };
const handlePrev = () => { const handlePrev = () => {
if (currentStep > 1) { if (currentStep > 1) {
setCurrentStep(currentStep - 1); setCurrentStep(currentStep - 1);
} }
}; };
return { return {
currentStep, currentStep,
templates, templates,
currentTemplate, currentTemplate,
configOperator, configOperator,
categoryOptions, categoryOptions,
setConfigOperator, setConfigOperator,
setCurrentTemplate, setCurrentTemplate,
setCurrentStep, setCurrentStep,
operators, operators,
setOperators, setOperators,
selectedOperators, selectedOperators,
setSelectedOperators, setSelectedOperators,
handleConfigChange, handleConfigChange,
toggleOperator, toggleOperator,
removeOperator, removeOperator,
handleNext, handleNext,
handlePrev, handlePrev,
}; };
} }

View File

@@ -1,223 +1,223 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import {Breadcrumb, App, Tabs} from "antd"; import {Breadcrumb, App, Tabs} from "antd";
import { import {
Play, Play,
Pause, Pause,
Clock, Clock,
CheckCircle, CheckCircle,
AlertCircle, AlertCircle,
Trash2, Trash2,
Activity, LayoutList, Activity, LayoutList,
} from "lucide-react"; } from "lucide-react";
import DetailHeader from "@/components/DetailHeader"; import DetailHeader from "@/components/DetailHeader";
import { Link, useNavigate, useParams } from "react-router"; import { Link, useNavigate, useParams } from "react-router";
import { import {
deleteCleaningTaskByIdUsingDelete, deleteCleaningTaskByIdUsingDelete,
executeCleaningTaskUsingPost, executeCleaningTaskUsingPost,
queryCleaningTaskByIdUsingGet, queryCleaningTaskLogByIdUsingGet, queryCleaningTaskResultByIdUsingGet, queryCleaningTaskByIdUsingGet, queryCleaningTaskLogByIdUsingGet, queryCleaningTaskResultByIdUsingGet,
stopCleaningTaskUsingPost, stopCleaningTaskUsingPost,
} from "../cleansing.api"; } from "../cleansing.api";
import {mapTask, TaskStatusMap} from "../cleansing.const"; import {mapTask, TaskStatusMap} from "../cleansing.const";
import {CleansingResult, TaskStatus} from "@/pages/DataCleansing/cleansing.model"; import {CleansingResult, TaskStatus} from "@/pages/DataCleansing/cleansing.model";
import BasicInfo from "./components/BasicInfo"; import BasicInfo from "./components/BasicInfo";
import OperatorTable from "./components/OperatorTable"; import OperatorTable from "./components/OperatorTable";
import FileTable from "./components/FileTable"; import FileTable from "./components/FileTable";
import LogsTable from "./components/LogsTable"; import LogsTable from "./components/LogsTable";
import {formatExecutionDuration} from "@/utils/unit.ts"; import {formatExecutionDuration} from "@/utils/unit.ts";
import {ReloadOutlined} from "@ant-design/icons"; import {ReloadOutlined} from "@ant-design/icons";
// 任务详情页面组件 // 任务详情页面组件
export default function CleansingTaskDetail() { export default function CleansingTaskDetail() {
const { id = "" } = useParams(); // 获取动态路由参数 const { id = "" } = useParams(); // 获取动态路由参数
const { message } = App.useApp(); const { message } = App.useApp();
const navigate = useNavigate(); const navigate = useNavigate();
const fetchTaskDetail = async () => { const fetchTaskDetail = async () => {
if (!id) return; if (!id) return;
try { try {
const { data } = await queryCleaningTaskByIdUsingGet(id); const { data } = await queryCleaningTaskByIdUsingGet(id);
setTask(mapTask(data)); setTask(mapTask(data));
} catch (error) { } catch (error) {
message.error("获取任务详情失败"); message.error("获取任务详情失败");
navigate("/data/cleansing"); navigate("/data/cleansing");
} }
}; };
const pauseTask = async () => { const pauseTask = async () => {
await stopCleaningTaskUsingPost(id); await stopCleaningTaskUsingPost(id);
message.success("任务已暂停"); message.success("任务已暂停");
fetchTaskDetail(); fetchTaskDetail();
}; };
const startTask = async () => { const startTask = async () => {
await executeCleaningTaskUsingPost(id); await executeCleaningTaskUsingPost(id);
message.success("任务已启动"); message.success("任务已启动");
fetchTaskDetail(); fetchTaskDetail();
}; };
const deleteTask = async () => { const deleteTask = async () => {
await deleteCleaningTaskByIdUsingDelete(id); await deleteCleaningTaskByIdUsingDelete(id);
message.success("任务已删除"); message.success("任务已删除");
navigate("/data/cleansing"); navigate("/data/cleansing");
}; };
const [result, setResult] = useState<CleansingResult[]>(); const [result, setResult] = useState<CleansingResult[]>();
const fetchTaskResult = async () => { const fetchTaskResult = async () => {
if (!id) return; if (!id) return;
try { try {
const { data } = await queryCleaningTaskResultByIdUsingGet(id); const { data } = await queryCleaningTaskResultByIdUsingGet(id);
setResult(data); setResult(data);
} catch (error) { } catch (error) {
message.error("获取清洗结果失败"); message.error("获取清洗结果失败");
navigate("/data/cleansing/task-detail/" + id); navigate("/data/cleansing/task-detail/" + id);
} }
}; };
const [taskLog, setTaskLog] = useState(); const [taskLog, setTaskLog] = useState();
const fetchTaskLog = async () => { const fetchTaskLog = async () => {
if (!id) return; if (!id) return;
try { try {
const { data } = await queryCleaningTaskLogByIdUsingGet(id); const { data } = await queryCleaningTaskLogByIdUsingGet(id);
setTaskLog(data); setTaskLog(data);
} catch (error) { } catch (error) {
message.error("获取清洗日志失败"); message.error("获取清洗日志失败");
navigate("/data/cleansing/task-detail/" + id); navigate("/data/cleansing/task-detail/" + id);
} }
}; };
const handleRefresh = async () => { const handleRefresh = async () => {
fetchTaskDetail(); fetchTaskDetail();
{activeTab === "files" && await fetchTaskResult()} {activeTab === "files" && await fetchTaskResult()}
{activeTab === "logs" && await fetchTaskLog()} {activeTab === "logs" && await fetchTaskLog()}
}; };
useEffect(() => { useEffect(() => {
fetchTaskDetail(); fetchTaskDetail();
}, [id]); }, [id]);
const [task, setTask] = useState(null); const [task, setTask] = useState(null);
const [activeTab, setActiveTab] = useState("basic"); const [activeTab, setActiveTab] = useState("basic");
const headerData = { const headerData = {
...task, ...task,
icon: <LayoutList className="w-8 h-8" />, icon: <LayoutList className="w-8 h-8" />,
status: TaskStatusMap[task?.status], status: TaskStatusMap[task?.status],
createdAt: task?.createdAt, createdAt: task?.createdAt,
lastUpdated: task?.updatedAt, lastUpdated: task?.updatedAt,
}; };
const statistics = [ const statistics = [
{ {
icon: <Clock className="w-4 h-4 text-blue-500" />, icon: <Clock className="w-4 h-4 text-blue-500" />,
label: "总耗时", label: "总耗时",
value: formatExecutionDuration(task?.startedAt, task?.finishedAt) || "--", value: formatExecutionDuration(task?.startedAt, task?.finishedAt) || "--",
}, },
{ {
icon: <CheckCircle className="w-4 h-4 text-green-500" />, icon: <CheckCircle className="w-4 h-4 text-green-500" />,
label: "成功文件", label: "成功文件",
value: task?.progress?.succeedFileNum || "0", value: task?.progress?.succeedFileNum || "0",
}, },
{ {
icon: <AlertCircle className="w-4 h-4 text-red-500" />, icon: <AlertCircle className="w-4 h-4 text-red-500" />,
label: "失败文件", label: "失败文件",
value: (task?.status.value === TaskStatus.RUNNING || task?.status.value === TaskStatus.PENDING) ? value: (task?.status.value === TaskStatus.RUNNING || task?.status.value === TaskStatus.PENDING) ?
task?.progress.failedFileNum : task?.progress.failedFileNum :
task?.progress?.totalFileNum - task?.progress.succeedFileNum, task?.progress?.totalFileNum - task?.progress.succeedFileNum,
}, },
{ {
icon: <Activity className="w-4 h-4 text-purple-500" />, icon: <Activity className="w-4 h-4 text-purple-500" />,
label: "成功率", label: "成功率",
value: task?.progress?.successRate ? task?.progress?.successRate + "%" : "--", value: task?.progress?.successRate ? task?.progress?.successRate + "%" : "--",
}, },
]; ];
const operations = [ const operations = [
...(task?.status === TaskStatus.RUNNING ...(task?.status === TaskStatus.RUNNING
? [ ? [
{ {
key: "pause", key: "pause",
label: "暂停任务", label: "暂停任务",
icon: <Pause className="w-4 h-4" />, icon: <Pause className="w-4 h-4" />,
onClick: pauseTask, onClick: pauseTask,
}, },
] ]
: []), : []),
...([TaskStatus.PENDING, TaskStatus.STOPPED, TaskStatus.FAILED].includes(task?.status?.value) ...([TaskStatus.PENDING, TaskStatus.STOPPED, TaskStatus.FAILED].includes(task?.status?.value)
? [ ? [
{ {
key: "start", key: "start",
label: "执行任务", label: "执行任务",
icon: <Play className="w-4 h-4" />, icon: <Play className="w-4 h-4" />,
onClick: startTask, onClick: startTask,
}, },
] ]
: []), : []),
{ {
key: "refresh", key: "refresh",
label: "更新任务", label: "更新任务",
icon: <ReloadOutlined className="w-4 h-4" />, icon: <ReloadOutlined className="w-4 h-4" />,
onClick: handleRefresh, onClick: handleRefresh,
}, },
{ {
key: "delete", key: "delete",
label: "删除任务", label: "删除任务",
icon: <Trash2 className="w-4 h-4" />, icon: <Trash2 className="w-4 h-4" />,
danger: true, danger: true,
onClick: deleteTask, onClick: deleteTask,
}, },
]; ];
const tabList = [ const tabList = [
{ {
key: "basic", key: "basic",
label: "基本信息", label: "基本信息",
}, },
{ {
key: "operators", key: "operators",
label: "处理算子", label: "处理算子",
}, },
{ {
key: "files", key: "files",
label: "处理文件", label: "处理文件",
}, },
{ {
key: "logs", key: "logs",
label: "运行日志", label: "运行日志",
}, },
]; ];
const breadItems = [ const breadItems = [
{ {
title: <Link to="/data/cleansing"></Link>, title: <Link to="/data/cleansing"></Link>,
}, },
{ {
title: "清洗任务详情", title: "清洗任务详情",
}, },
]; ];
return ( return (
<> <>
<Breadcrumb items={breadItems} /> <Breadcrumb items={breadItems} />
<div className="mb-4 mt-4"> <div className="mb-4 mt-4">
<DetailHeader <DetailHeader
data={headerData} data={headerData}
statistics={statistics} statistics={statistics}
operations={operations} operations={operations}
/> />
</div> </div>
<div className="flex-overflow-auto p-6 pt-2 bg-white rounded-md shadow"> <div className="flex-overflow-auto p-6 pt-2 bg-white rounded-md shadow">
<Tabs activeKey={activeTab} items={tabList} onChange={setActiveTab} /> <Tabs activeKey={activeTab} items={tabList} onChange={setActiveTab} />
<div className="h-full flex-1 overflow-auto"> <div className="h-full flex-1 overflow-auto">
{activeTab === "basic" && ( {activeTab === "basic" && (
<BasicInfo task={task} /> <BasicInfo task={task} />
)} )}
{activeTab === "operators" && <OperatorTable task={task} />} {activeTab === "operators" && <OperatorTable task={task} />}
{activeTab === "files" && <FileTable result={result} fetchTaskResult={fetchTaskResult} />} {activeTab === "files" && <FileTable result={result} fetchTaskResult={fetchTaskResult} />}
{activeTab === "logs" && <LogsTable taskLog={taskLog} fetchTaskLog={fetchTaskLog} />} {activeTab === "logs" && <LogsTable taskLog={taskLog} fetchTaskLog={fetchTaskLog} />}
</div> </div>
</div> </div>
</> </>
); );
} }

View File

@@ -1,122 +1,122 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import {Breadcrumb, App, Tabs} from "antd"; import {Breadcrumb, App, Tabs} from "antd";
import { import {
Trash2, Trash2,
LayoutList, LayoutList,
} from "lucide-react"; } from "lucide-react";
import DetailHeader from "@/components/DetailHeader"; import DetailHeader from "@/components/DetailHeader";
import { Link, useNavigate, useParams } from "react-router"; import { Link, useNavigate, useParams } from "react-router";
import { import {
deleteCleaningTemplateByIdUsingDelete, deleteCleaningTemplateByIdUsingDelete,
queryCleaningTemplateByIdUsingGet, queryCleaningTemplateByIdUsingGet,
} from "../cleansing.api"; } from "../cleansing.api";
import {mapTemplate} from "../cleansing.const"; import {mapTemplate} from "../cleansing.const";
import OperatorTable from "./components/OperatorTable"; import OperatorTable from "./components/OperatorTable";
import {EditOutlined, ReloadOutlined, NumberOutlined} from "@ant-design/icons"; import {EditOutlined, ReloadOutlined, NumberOutlined} from "@ant-design/icons";
// 任务详情页面组件 // 任务详情页面组件
export default function CleansingTemplateDetail() { export default function CleansingTemplateDetail() {
const { id = "" } = useParams(); // 获取动态路由参数 const { id = "" } = useParams(); // 获取动态路由参数
const { message } = App.useApp(); const { message } = App.useApp();
const navigate = useNavigate(); const navigate = useNavigate();
const [template, setTemplate] = useState(); const [template, setTemplate] = useState();
const fetchTemplateDetail = async () => { const fetchTemplateDetail = async () => {
if (!id) return; if (!id) return;
try { try {
const { data } = await queryCleaningTemplateByIdUsingGet(id); const { data } = await queryCleaningTemplateByIdUsingGet(id);
setTemplate(mapTemplate(data)); setTemplate(mapTemplate(data));
} catch (error) { } catch (error) {
message.error("获取任务详情失败"); message.error("获取任务详情失败");
navigate("/data/cleansing"); navigate("/data/cleansing");
} }
}; };
const deleteTemplate = async () => { const deleteTemplate = async () => {
await deleteCleaningTemplateByIdUsingDelete(id); await deleteCleaningTemplateByIdUsingDelete(id);
message.success("模板已删除"); message.success("模板已删除");
navigate("/data/cleansing"); navigate("/data/cleansing");
}; };
const handleRefresh = async () => { const handleRefresh = async () => {
fetchTemplateDetail(); fetchTemplateDetail();
}; };
useEffect(() => { useEffect(() => {
fetchTemplateDetail(); fetchTemplateDetail();
}, [id]); }, [id]);
const [activeTab, setActiveTab] = useState("operators"); const [activeTab, setActiveTab] = useState("operators");
const headerData = { const headerData = {
...template, ...template,
icon: <LayoutList className="w-8 h-8" />, icon: <LayoutList className="w-8 h-8" />,
createdAt: template?.createdAt, createdAt: template?.createdAt,
lastUpdated: template?.updatedAt, lastUpdated: template?.updatedAt,
}; };
const statistics = [ const statistics = [
{ {
icon: <NumberOutlined className="w-4 h-4 text-green-500" />, icon: <NumberOutlined className="w-4 h-4 text-green-500" />,
label: "算子数量", label: "算子数量",
value: template?.instance?.length || 0, value: template?.instance?.length || 0,
}, },
]; ];
const operations = [ const operations = [
{ {
key: "update", key: "update",
label: "更新任务", label: "更新任务",
icon: <EditOutlined className="w-4 h-4" />, icon: <EditOutlined className="w-4 h-4" />,
onClick: () => navigate(`/data/cleansing/update-template/${id}`), onClick: () => navigate(`/data/cleansing/update-template/${id}`),
}, },
{ {
key: "refresh", key: "refresh",
label: "更新任务", label: "更新任务",
icon: <ReloadOutlined className="w-4 h-4" />, icon: <ReloadOutlined className="w-4 h-4" />,
onClick: handleRefresh, onClick: handleRefresh,
}, },
{ {
key: "delete", key: "delete",
label: "删除任务", label: "删除任务",
icon: <Trash2 className="w-4 h-4" />, icon: <Trash2 className="w-4 h-4" />,
danger: true, danger: true,
onClick: deleteTemplate, onClick: deleteTemplate,
}, },
]; ];
const tabList = [ const tabList = [
{ {
key: "operators", key: "operators",
label: "处理算子", label: "处理算子",
}, },
]; ];
const breadItems = [ const breadItems = [
{ {
title: <Link to="/data/cleansing"></Link>, title: <Link to="/data/cleansing"></Link>,
}, },
{ {
title: "模板详情", title: "模板详情",
}, },
]; ];
return ( return (
<> <>
<Breadcrumb items={breadItems} /> <Breadcrumb items={breadItems} />
<div className="mb-4 mt-4"> <div className="mb-4 mt-4">
<DetailHeader <DetailHeader
data={headerData} data={headerData}
statistics={statistics} statistics={statistics}
operations={operations} operations={operations}
/> />
</div> </div>
<div className="flex-overflow-auto p-6 pt-2 bg-white rounded-md shadow"> <div className="flex-overflow-auto p-6 pt-2 bg-white rounded-md shadow">
<Tabs activeKey={activeTab} items={tabList} onChange={setActiveTab} /> <Tabs activeKey={activeTab} items={tabList} onChange={setActiveTab} />
<div className="h-full flex-1 overflow-auto"> <div className="h-full flex-1 overflow-auto">
<OperatorTable task={template} /> <OperatorTable task={template} />
</div> </div>
</div> </div>
</> </>
); );
} }

View File

@@ -1,138 +1,138 @@
import {CleansingTask, TaskStatus} from "@/pages/DataCleansing/cleansing.model"; import {CleansingTask, TaskStatus} from "@/pages/DataCleansing/cleansing.model";
import { Button, Card, Descriptions, Progress } from "antd"; import { Button, Card, Descriptions, Progress } from "antd";
import { Activity, AlertCircle, CheckCircle, Clock } from "lucide-react"; import { Activity, AlertCircle, CheckCircle, Clock } from "lucide-react";
import { useNavigate } from "react-router"; import { useNavigate } from "react-router";
import {formatExecutionDuration} from "@/utils/unit.ts"; import {formatExecutionDuration} from "@/utils/unit.ts";
export default function BasicInfo({ task }: { task: CleansingTask }) { export default function BasicInfo({ task }: { task: CleansingTask }) {
const navigate = useNavigate(); const navigate = useNavigate();
const descriptionItems = [ const descriptionItems = [
{ {
key: "id", key: "id",
label: "任务ID", label: "任务ID",
children: <span className="font-mono">{task?.id}</span>, children: <span className="font-mono">{task?.id}</span>,
}, },
{ key: "name", label: "任务名称", children: task?.name }, { key: "name", label: "任务名称", children: task?.name },
{ {
key: "dataset", key: "dataset",
label: "源数据集", label: "源数据集",
children: ( children: (
<Button <Button
style={{ paddingLeft: 0, marginLeft: 0 }} style={{ paddingLeft: 0, marginLeft: 0 }}
type="link" type="link"
size="small" size="small"
onClick={() => onClick={() =>
navigate("/data/management/detail/" + task?.srcDatasetId) navigate("/data/management/detail/" + task?.srcDatasetId)
} }
> >
{task?.srcDatasetName} {task?.srcDatasetName}
</Button> </Button>
), ),
}, },
{ {
key: "targetDataset", key: "targetDataset",
label: "目标数据集", label: "目标数据集",
children: ( children: (
<Button <Button
style={{ paddingLeft: 0, marginLeft: 0 }} style={{ paddingLeft: 0, marginLeft: 0 }}
type="link" type="link"
size="small" size="small"
onClick={() => onClick={() =>
navigate("/data/management/detail/" + task?.destDatasetId) navigate("/data/management/detail/" + task?.destDatasetId)
} }
> >
{task?.destDatasetName} {task?.destDatasetName}
</Button> </Button>
), ),
}, },
{ key: "startTime", label: "开始时间", children: task?.startedAt }, { key: "startTime", label: "开始时间", children: task?.startedAt },
{ {
key: "description", key: "description",
label: "任务描述", label: "任务描述",
children: ( children: (
<span className="text-gray-600">{task?.description || "--"}</span> <span className="text-gray-600">{task?.description || "--"}</span>
), ),
span: 2, span: 2,
}, },
]; ];
return ( return (
<> <>
{/* 执行摘要 */} {/* 执行摘要 */}
<Card className="mb-6"> <Card className="mb-6">
<div className="grid grid-cols-4 gap-4"> <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"> <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" /> <Clock className="w-8 h-8 text-blue-500 mb-2 mx-auto" />
<div className="text-xl font-bold text-blue-500"> <div className="text-xl font-bold text-blue-500">
{formatExecutionDuration(task?.startedAt, task?.finishedAt) || "--"} {formatExecutionDuration(task?.startedAt, task?.finishedAt) || "--"}
</div> </div>
<div className="text-sm text-gray-600"></div> <div className="text-sm text-gray-600"></div>
</div> </div>
<div className="text-center p-4 bg-gradient-to-br from-green-50 to-green-100 rounded-lg"> <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" /> <CheckCircle className="w-8 h-8 text-green-500 mb-2 mx-auto" />
<div className="text-xl font-bold text-green-500"> <div className="text-xl font-bold text-green-500">
{task?.progress?.succeedFileNum || "0"} {task?.progress?.succeedFileNum || "0"}
</div> </div>
<div className="text-sm text-gray-600"></div> <div className="text-sm text-gray-600"></div>
</div> </div>
<div className="text-center p-4 bg-gradient-to-br from-red-50 to-red-100 rounded-lg"> <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" /> <AlertCircle className="w-8 h-8 text-red-500 mb-2 mx-auto" />
<div className="text-xl font-bold text-red-500"> <div className="text-xl font-bold text-red-500">
{(task?.status.value === TaskStatus.RUNNING || task?.status.value === TaskStatus.PENDING) ? {(task?.status.value === TaskStatus.RUNNING || task?.status.value === TaskStatus.PENDING) ?
task?.progress.failedFileNum : task?.progress.failedFileNum :
task?.progress?.totalFileNum - task?.progress.succeedFileNum} task?.progress?.totalFileNum - task?.progress.succeedFileNum}
</div> </div>
<div className="text-sm text-gray-600"></div> <div className="text-sm text-gray-600"></div>
</div> </div>
<div className="text-center p-4 bg-gradient-to-br from-purple-50 to-purple-100 rounded-lg"> <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" /> <Activity className="w-8 h-8 text-purple-500 mb-2 mx-auto" />
<div className="text-xl font-bold text-purple-500"> <div className="text-xl font-bold text-purple-500">
{task?.progress?.successRate ? task?.progress?.successRate + "%" : "--"} {task?.progress?.successRate ? task?.progress?.successRate + "%" : "--"}
</div> </div>
<div className="text-sm text-gray-600"></div> <div className="text-sm text-gray-600"></div>
</div> </div>
</div> </div>
</Card> </Card>
{/* 基本信息 */} {/* 基本信息 */}
<Card> <Card>
<div className="mb-8"> <div className="mb-8">
<h3 className="text-lg font-semibold text-gray-900 mb-4"></h3> <h3 className="text-lg font-semibold text-gray-900 mb-4"></h3>
<Descriptions <Descriptions
column={2} column={2}
bordered={false} bordered={false}
size="middle" size="middle"
labelStyle={{ fontWeight: 500, color: "#555" }} labelStyle={{ fontWeight: 500, color: "#555" }}
contentStyle={{ fontSize: 14 }} contentStyle={{ fontSize: 14 }}
items={descriptionItems} items={descriptionItems}
></Descriptions> ></Descriptions>
</div> </div>
{/* 处理进度 */} {/* 处理进度 */}
<div> <div>
<h3 className="text-lg font-semibold text-gray-900 mb-4"></h3> <h3 className="text-lg font-semibold text-gray-900 mb-4"></h3>
{ task?.status?.value === TaskStatus.FAILED ? { task?.status?.value === TaskStatus.FAILED ?
<Progress percent={task?.progress?.process} size="small" status="exception" /> <Progress percent={task?.progress?.process} size="small" status="exception" />
: <Progress percent={task?.progress?.process} size="small"/> : <Progress percent={task?.progress?.process} size="small"/>
} }
<div className="grid grid-cols-2 gap-4 text-sm mt-4"> <div className="grid grid-cols-2 gap-4 text-sm mt-4">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="w-3 h-3 bg-green-500 rounded-full inline-block" /> <span className="w-3 h-3 bg-green-500 rounded-full inline-block" />
<span>: {task?.progress?.succeedFileNum || "0"}</span> <span>: {task?.progress?.succeedFileNum || "0"}</span>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="w-3 h-3 bg-blue-500 rounded-full inline-block" /> <span className="w-3 h-3 bg-blue-500 rounded-full inline-block" />
<span>: {(task?.status.value === TaskStatus.RUNNING || task?.status.value === TaskStatus.PENDING) ? <span>: {(task?.status.value === TaskStatus.RUNNING || task?.status.value === TaskStatus.PENDING) ?
task?.progress?.totalFileNum - task?.progress.succeedFileNum : 0}</span> task?.progress?.totalFileNum - task?.progress.succeedFileNum : 0}</span>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="w-3 h-3 bg-red-500 rounded-full inline-block" /> <span className="w-3 h-3 bg-red-500 rounded-full inline-block" />
<span>: {(task?.status.value === TaskStatus.RUNNING || task?.status.value === TaskStatus.PENDING) ? <span>: {(task?.status.value === TaskStatus.RUNNING || task?.status.value === TaskStatus.PENDING) ?
task?.progress.failedFileNum : task?.progress.failedFileNum :
task?.progress?.totalFileNum - task?.progress.succeedFileNum}</span> task?.progress?.totalFileNum - task?.progress.succeedFileNum}</span>
</div> </div>
</div> </div>
</div> </div>
</Card> </Card>
</> </>
); );
} }

View File

@@ -1,397 +1,397 @@
import {Button, Modal, Table, Badge, Input, Popover} from "antd"; import {Button, Modal, Table, Badge, Input, Popover} from "antd";
import { Download } from "lucide-react"; import { Download } from "lucide-react";
import {useEffect, useState} from "react"; import {useEffect, useState} from "react";
import {useParams} from "react-router"; import {useParams} from "react-router";
import {TaskStatus} from "@/pages/DataCleansing/cleansing.model.ts"; import {TaskStatus} from "@/pages/DataCleansing/cleansing.model.ts";
import {TaskStatusMap} from "@/pages/DataCleansing/cleansing.const.tsx"; import {TaskStatusMap} from "@/pages/DataCleansing/cleansing.const.tsx";
// 模拟文件列表数据 // 模拟文件列表数据
export default function FileTable({result, fetchTaskResult}) { export default function FileTable({result, fetchTaskResult}) {
const { id = "" } = useParams(); const { id = "" } = useParams();
const [showFileCompareDialog, setShowFileCompareDialog] = useState(false); const [showFileCompareDialog, setShowFileCompareDialog] = useState(false);
const [selectedFile, setSelectedFile] = useState<any>(null); const [selectedFile, setSelectedFile] = useState<any>(null);
const [selectedFileIds, setSelectedFileIds] = useState<string[]>([]); const [selectedFileIds, setSelectedFileIds] = useState<string[]>([]);
useEffect(() => { useEffect(() => {
fetchTaskResult(); fetchTaskResult();
}, [id]); }, [id]);
const handleSelectAllFiles = (checked: boolean) => { const handleSelectAllFiles = (checked: boolean) => {
if (checked) { if (checked) {
setSelectedFileIds(result.map((file) => file.instanceId)); setSelectedFileIds(result.map((file) => file.instanceId));
} else { } else {
setSelectedFileIds([]); setSelectedFileIds([]);
} }
}; };
const handleSelectFile = (fileId: string, checked: boolean) => { const handleSelectFile = (fileId: string, checked: boolean) => {
if (checked) { if (checked) {
setSelectedFileIds([...selectedFileIds, fileId]); setSelectedFileIds([...selectedFileIds, fileId]);
} else { } else {
setSelectedFileIds(selectedFileIds.filter((id) => id !== fileId)); setSelectedFileIds(selectedFileIds.filter((id) => id !== fileId));
} }
}; };
const handleViewFileCompare = (file: any) => { const handleViewFileCompare = (file: any) => {
setSelectedFile(file); setSelectedFile(file);
setShowFileCompareDialog(true); setShowFileCompareDialog(true);
}; };
const handleBatchDownload = () => { const handleBatchDownload = () => {
// 实际下载逻辑 // 实际下载逻辑
}; };
function formatFileSize(bytes: number, decimals: number = 2): string { function formatFileSize(bytes: number, decimals: number = 2): string {
if (bytes === 0) return '0 Bytes'; if (bytes === 0) return '0 Bytes';
const k = 1024; const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
const i = Math.floor(Math.log(bytes) / Math.log(k)); const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(decimals)) + ' ' + sizes[i]; return parseFloat((bytes / Math.pow(k, i)).toFixed(decimals)) + ' ' + sizes[i];
} }
const fileColumns = [ const fileColumns = [
{ {
title: ( title: (
<input <input
type="checkbox" type="checkbox"
checked={ checked={
selectedFileIds.length === result?.length && result?.length > 0 selectedFileIds.length === result?.length && result?.length > 0
} }
onChange={(e) => handleSelectAllFiles(e.target.checked)} onChange={(e) => handleSelectAllFiles(e.target.checked)}
className="w-4 h-4" className="w-4 h-4"
/> />
), ),
dataIndex: "select", dataIndex: "select",
key: "select", key: "select",
width: 50, width: 50,
render: (_text: string, record: any) => ( render: (_text: string, record: any) => (
<input <input
type="checkbox" type="checkbox"
checked={selectedFileIds.includes(record.id)} checked={selectedFileIds.includes(record.id)}
onChange={(e) => handleSelectFile(record.id, e.target.checked)} onChange={(e) => handleSelectFile(record.id, e.target.checked)}
className="w-4 h-4" className="w-4 h-4"
/> />
), ),
}, },
{ {
title: "文件名", title: "文件名",
dataIndex: "srcName", dataIndex: "srcName",
key: "srcName", key: "srcName",
width: 200, width: 200,
filterDropdown: ({ filterDropdown: ({
setSelectedKeys, setSelectedKeys,
selectedKeys, selectedKeys,
confirm, confirm,
clearFilters, clearFilters,
}: any) => ( }: any) => (
<div className="p-4 w-64"> <div className="p-4 w-64">
<Input <Input
placeholder="搜索文件名" placeholder="搜索文件名"
value={selectedKeys[0]} value={selectedKeys[0]}
onChange={(e) => onChange={(e) =>
setSelectedKeys(e.target.value ? [e.target.value] : []) setSelectedKeys(e.target.value ? [e.target.value] : [])
} }
onPressEnter={() => confirm()} onPressEnter={() => confirm()}
className="mb-2" className="mb-2"
/> />
<div className="flex gap-2"> <div className="flex gap-2">
<Button size="small" onClick={() => confirm()}> <Button size="small" onClick={() => confirm()}>
</Button> </Button>
<Button size="small" onClick={() => clearFilters()}> <Button size="small" onClick={() => clearFilters()}>
</Button> </Button>
</div> </div>
</div> </div>
), ),
onFilter: (value: string, record: any) => onFilter: (value: string, record: any) =>
record.srcName.toLowerCase().includes(value.toLowerCase()), record.srcName.toLowerCase().includes(value.toLowerCase()),
render: (text: string) => ( render: (text: string) => (
<span>{text?.replace(/\.[^/.]+$/, "")}</span> <span>{text?.replace(/\.[^/.]+$/, "")}</span>
), ),
}, },
{ {
title: "清洗后文件名", title: "清洗后文件名",
dataIndex: "destName", dataIndex: "destName",
key: "destName", key: "destName",
width: 200, width: 200,
filterDropdown: ({ filterDropdown: ({
setSelectedKeys, setSelectedKeys,
selectedKeys, selectedKeys,
confirm, confirm,
clearFilters, clearFilters,
}: any) => ( }: any) => (
<div className="p-4 w-64"> <div className="p-4 w-64">
<Input <Input
placeholder="搜索文件名" placeholder="搜索文件名"
value={selectedKeys[0]} value={selectedKeys[0]}
onChange={(e) => onChange={(e) =>
setSelectedKeys(e.target.value ? [e.target.value] : []) setSelectedKeys(e.target.value ? [e.target.value] : [])
} }
onPressEnter={() => confirm()} onPressEnter={() => confirm()}
className="mb-2" className="mb-2"
/> />
<div className="flex gap-2"> <div className="flex gap-2">
<Button size="small" onClick={() => confirm()}> <Button size="small" onClick={() => confirm()}>
</Button> </Button>
<Button size="small" onClick={() => clearFilters()}> <Button size="small" onClick={() => clearFilters()}>
</Button> </Button>
</div> </div>
</div> </div>
), ),
onFilter: (value: string, record: any) => onFilter: (value: string, record: any) =>
record.destName.toLowerCase().includes(value.toLowerCase()), record.destName.toLowerCase().includes(value.toLowerCase()),
render: (text: string) => ( render: (text: string) => (
<span>{text?.replace(/\.[^/.]+$/, "")}</span> <span>{text?.replace(/\.[^/.]+$/, "")}</span>
), ),
}, },
{ {
title: "文件类型", title: "文件类型",
dataIndex: "srcType", dataIndex: "srcType",
key: "srcType", key: "srcType",
filterDropdown: ({ filterDropdown: ({
setSelectedKeys, setSelectedKeys,
selectedKeys, selectedKeys,
confirm, confirm,
clearFilters, clearFilters,
}: any) => ( }: any) => (
<div className="p-4 w-64"> <div className="p-4 w-64">
<Input <Input
placeholder="搜索文件类型" placeholder="搜索文件类型"
value={selectedKeys[0]} value={selectedKeys[0]}
onChange={(e) => onChange={(e) =>
setSelectedKeys(e.target.value ? [e.target.value] : []) setSelectedKeys(e.target.value ? [e.target.value] : [])
} }
onPressEnter={() => confirm()} onPressEnter={() => confirm()}
className="mb-2" className="mb-2"
/> />
<div className="flex gap-2"> <div className="flex gap-2">
<Button size="small" onClick={() => confirm()}> <Button size="small" onClick={() => confirm()}>
</Button> </Button>
<Button size="small" onClick={() => clearFilters()}> <Button size="small" onClick={() => clearFilters()}>
</Button> </Button>
</div> </div>
</div> </div>
), ),
onFilter: (value: string, record: any) => onFilter: (value: string, record: any) =>
record.srcType.toLowerCase().includes(value.toLowerCase()), record.srcType.toLowerCase().includes(value.toLowerCase()),
render: (text: string) => ( render: (text: string) => (
<span className="font-mono text-sm">{text}</span> <span className="font-mono text-sm">{text}</span>
), ),
}, },
{ {
title: "清洗后文件类型", title: "清洗后文件类型",
dataIndex: "destType", dataIndex: "destType",
key: "destType", key: "destType",
filterDropdown: ({ filterDropdown: ({
setSelectedKeys, setSelectedKeys,
selectedKeys, selectedKeys,
confirm, confirm,
clearFilters, clearFilters,
}: any) => ( }: any) => (
<div className="p-4 w-64"> <div className="p-4 w-64">
<Input <Input
placeholder="搜索文件类型" placeholder="搜索文件类型"
value={selectedKeys[0]} value={selectedKeys[0]}
onChange={(e) => onChange={(e) =>
setSelectedKeys(e.target.value ? [e.target.value] : []) setSelectedKeys(e.target.value ? [e.target.value] : [])
} }
onPressEnter={() => confirm()} onPressEnter={() => confirm()}
className="mb-2" className="mb-2"
/> />
<div className="flex gap-2"> <div className="flex gap-2">
<Button size="small" onClick={() => confirm()}> <Button size="small" onClick={() => confirm()}>
</Button> </Button>
<Button size="small" onClick={() => clearFilters()}> <Button size="small" onClick={() => clearFilters()}>
</Button> </Button>
</div> </div>
</div> </div>
), ),
onFilter: (value: string, record: any) => onFilter: (value: string, record: any) =>
record.destType.toLowerCase().includes(value.toLowerCase()), record.destType.toLowerCase().includes(value.toLowerCase()),
render: (text: string) => ( render: (text: string) => (
<span className="font-mono text-sm">{text}</span> <span className="font-mono text-sm">{text}</span>
), ),
}, },
{ {
title: "清洗前大小", title: "清洗前大小",
dataIndex: "srcSize", dataIndex: "srcSize",
key: "srcSize", key: "srcSize",
sorter: (a: any, b: any) => { sorter: (a: any, b: any) => {
const getSizeInBytes = (size: string) => { const getSizeInBytes = (size: string) => {
if (!size || size === "-") return 0; if (!size || size === "-") return 0;
const num = Number.parseFloat(size); const num = Number.parseFloat(size);
if (size.includes("GB")) return num * 1024 * 1024 * 1024; if (size.includes("GB")) return num * 1024 * 1024 * 1024;
if (size.includes("MB")) return num * 1024 * 1024; if (size.includes("MB")) return num * 1024 * 1024;
if (size.includes("KB")) return num * 1024; if (size.includes("KB")) return num * 1024;
return num; return num;
}; };
return getSizeInBytes(a.originalSize) - getSizeInBytes(b.originalSize); return getSizeInBytes(a.originalSize) - getSizeInBytes(b.originalSize);
}, },
render: (number: number) => ( render: (number: number) => (
<span className="font-mono text-sm">{formatFileSize(number)}</span> <span className="font-mono text-sm">{formatFileSize(number)}</span>
), ),
}, },
{ {
title: "清洗后大小", title: "清洗后大小",
dataIndex: "destSize", dataIndex: "destSize",
key: "destSize", key: "destSize",
sorter: (a: any, b: any) => { sorter: (a: any, b: any) => {
const getSizeInBytes = (size: string) => { const getSizeInBytes = (size: string) => {
if (!size || size === "-") return 0; if (!size || size === "-") return 0;
const num = Number.parseFloat(size); const num = Number.parseFloat(size);
if (size.includes("GB")) return num * 1024 * 1024 * 1024; if (size.includes("GB")) return num * 1024 * 1024 * 1024;
if (size.includes("MB")) return num * 1024 * 1024; if (size.includes("MB")) return num * 1024 * 1024;
if (size.includes("KB")) return num * 1024; if (size.includes("KB")) return num * 1024;
return num; return num;
}; };
return ( return (
getSizeInBytes(a.processedSize) - getSizeInBytes(b.processedSize) getSizeInBytes(a.processedSize) - getSizeInBytes(b.processedSize)
); );
}, },
render: (number: number) => ( render: (number: number) => (
<span className="font-mono text-sm">{formatFileSize(number)}</span> <span className="font-mono text-sm">{formatFileSize(number)}</span>
), ),
}, },
{ {
title: "状态", title: "状态",
dataIndex: "status", dataIndex: "status",
key: "status", key: "status",
filters: [ filters: [
{ text: "已完成", value: "COMPLETED" }, { text: "已完成", value: "COMPLETED" },
{ text: "失败", value: "FAILED" }, { text: "失败", value: "FAILED" },
], ],
onFilter: (value: string, record: any) => record.status === value, onFilter: (value: string, record: any) => record.status === value,
render: (status: string) => ( render: (status: string) => (
<Badge <Badge
status={ status={
status === "COMPLETED" status === "COMPLETED"
? "success" ? "success"
: "error" : "error"
} }
text={TaskStatusMap[status as TaskStatus].label} text={TaskStatusMap[status as TaskStatus].label}
/> />
), ),
}, },
{ {
title: "操作", title: "操作",
key: "action", key: "action",
width: 200, width: 200,
render: (_text: string, record: any) => ( render: (_text: string, record: any) => (
<div className="flex"> <div className="flex">
{record.status === "COMPLETED" ? ( {record.status === "COMPLETED" ? (
<Button <Button
type="link" type="link"
size="small" size="small"
onClick={() => handleViewFileCompare(record)} onClick={() => handleViewFileCompare(record)}
> >
</Button> </Button>
) : ( ) : (
<Button <Button
type="link" type="link"
size="small" size="small"
disabled disabled
> >
</Button> </Button>
)} )}
<Popover content="暂未开放"> <Popover content="暂未开放">
<Button type="link" size="small" disabled></Button> <Button type="link" size="small" disabled></Button>
</Popover> </Popover>
</div> </div>
), ),
}, },
]; ];
return ( return (
<> <>
{selectedFileIds.length > 0 && ( {selectedFileIds.length > 0 && (
<div className="mb-4 flex justify-between"> <div className="mb-4 flex justify-between">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-sm text-gray-600"> <span className="text-sm text-gray-600">
{selectedFileIds.length} {selectedFileIds.length}
</span> </span>
<Button <Button
onClick={handleBatchDownload} onClick={handleBatchDownload}
size="small" size="small"
type="primary" type="primary"
icon={<Download className="w-4 h-4 mr-2" />} icon={<Download className="w-4 h-4 mr-2" />}
> >
</Button> </Button>
</div> </div>
</div> </div>
)} )}
<Table <Table
columns={fileColumns} columns={fileColumns}
dataSource={result} dataSource={result}
pagination={{ pageSize: 10, showSizeChanger: true }} pagination={{ pageSize: 10, showSizeChanger: true }}
size="middle" size="middle"
rowKey="id" rowKey="id"
/> />
{/* 文件对比弹窗 */} {/* 文件对比弹窗 */}
<Modal <Modal
open={showFileCompareDialog} open={showFileCompareDialog}
onCancel={() => setShowFileCompareDialog(false)} onCancel={() => setShowFileCompareDialog(false)}
footer={null} footer={null}
width={900} width={900}
title={<span> - {selectedFile?.fileName}</span>} title={<span> - {selectedFile?.fileName}</span>}
> >
<div className="grid grid-cols-2 gap-6 py-6"> <div className="grid grid-cols-2 gap-6 py-6">
<div> <div>
<h4 className="font-medium text-gray-900"></h4> <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="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="text-center text-gray-500">
<div className="w-16 h-16 bg-gray-300 rounded-lg mx-auto mb-2" /> <div className="w-16 h-16 bg-gray-300 rounded-lg mx-auto mb-2" />
<div className="text-sm"></div> <div className="text-sm"></div>
<div className="text-xs text-gray-400"> <div className="text-xs text-gray-400">
: {formatFileSize(selectedFile?.srcSize)} : {formatFileSize(selectedFile?.srcSize)}
</div> </div>
</div> </div>
</div> </div>
<div className="text-sm text-gray-600 mt-3 space-y-1"> <div className="text-sm text-gray-600 mt-3 space-y-1">
<div> <div>
<span className="font-medium">:</span> {selectedFile?.srcType} <span className="font-medium">:</span> {selectedFile?.srcType}
</div> </div>
</div> </div>
</div> </div>
<div> <div>
<h4 className="font-medium text-gray-900"></h4> <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="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="text-center text-gray-500">
<div className="w-16 h-16 bg-blue-300 rounded-lg mx-auto mb-2" /> <div className="w-16 h-16 bg-blue-300 rounded-lg mx-auto mb-2" />
<div className="text-sm"></div> <div className="text-sm"></div>
<div className="text-xs text-gray-400"> <div className="text-xs text-gray-400">
: {formatFileSize(selectedFile?.destSize)} : {formatFileSize(selectedFile?.destSize)}
</div> </div>
</div> </div>
</div> </div>
<div className="text-sm text-gray-600 mt-3 space-y-1"> <div className="text-sm text-gray-600 mt-3 space-y-1">
<div> <div>
<span className="font-medium">:</span> {selectedFile?.destType} <span className="font-medium">:</span> {selectedFile?.destType}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div className="border-t border-gray-200 mt-6 pt-4"> <div className="border-t border-gray-200 mt-6 pt-4">
<h4 className="font-medium text-gray-900 mb-3"></h4> <h4 className="font-medium text-gray-900 mb-3"></h4>
<div className="grid grid-cols-3 gap-4 text-sm"> <div className="grid grid-cols-3 gap-4 text-sm">
<div className="bg-green-50 p-4 rounded-lg"> <div className="bg-green-50 p-4 rounded-lg">
<div className="font-medium text-green-700"></div> <div className="font-medium text-green-700"></div>
<div className="text-green-600"> {(100 * (selectedFile?.srcSize - selectedFile?.destSize) / selectedFile?.srcSize).toFixed(2)}%</div> <div className="text-green-600"> {(100 * (selectedFile?.srcSize - selectedFile?.destSize) / selectedFile?.srcSize).toFixed(2)}%</div>
</div> </div>
</div> </div>
</div> </div>
</Modal> </Modal>
</> </>
); );
} }

View File

@@ -1,43 +1,43 @@
import {useEffect} from "react"; import {useEffect} from "react";
import {useParams} from "react-router"; import {useParams} from "react-router";
import {FileClock} from "lucide-react"; import {FileClock} from "lucide-react";
export default function LogsTable({taskLog, fetchTaskLog} : {taskLog: any[], fetchTaskLog: () => Promise<any>}) { export default function LogsTable({taskLog, fetchTaskLog} : {taskLog: any[], fetchTaskLog: () => Promise<any>}) {
const { id = "" } = useParams(); const { id = "" } = useParams();
useEffect(() => { useEffect(() => {
fetchTaskLog(); fetchTaskLog();
}, [id]); }, [id]);
return taskLog?.length > 0 ? ( return taskLog?.length > 0 ? (
<> <>
<div className="text-gray-300 p-4 border border-gray-700 bg-gray-800 rounded-lg"> <div className="text-gray-300 p-4 border border-gray-700 bg-gray-800 rounded-lg">
<div className="font-mono text-sm"> <div className="font-mono text-sm">
{taskLog?.map?.((log, index) => ( {taskLog?.map?.((log, index) => (
<div key={index} className="flex gap-3"> <div key={index} className="flex gap-3">
<span <span
className={`min-w-20 ${ className={`min-w-20 ${
log.level === "ERROR" || log.level === "FATAL" log.level === "ERROR" || log.level === "FATAL"
? "text-red-500" ? "text-red-500"
: log.level === "WARNING" || log.level === "WARN" : log.level === "WARNING" || log.level === "WARN"
? "text-yellow-500" ? "text-yellow-500"
: "text-green-500" : "text-green-500"
}`} }`}
> >
[{log.level}] [{log.level}]
</span> </span>
<span className="text-gray-100">{log.message}</span> <span className="text-gray-100">{log.message}</span>
</div> </div>
))} ))}
</div> </div>
</div> </div>
</> </>
) : ( ) : (
<div className="text-center py-12"> <div className="text-center py-12">
<FileClock className="w-16 h-16 text-gray-300 mx-auto mb-4" /> <FileClock className="w-16 h-16 text-gray-300 mx-auto mb-4" />
<h3 className="text-lg font-medium text-gray-900 mb-2"> <h3 className="text-lg font-medium text-gray-900 mb-2">
</h3> </h3>
</div> </div>
); );
} }

View File

@@ -1,25 +1,25 @@
import {Steps, Typography} from "antd"; import {Steps, Typography} from "antd";
import {useNavigate} from "react-router"; import {useNavigate} from "react-router";
export default function OperatorTable({ task }: { task: any }) { export default function OperatorTable({ task }: { task: any }) {
const navigate = useNavigate(); const navigate = useNavigate();
return task?.instance?.length > 0 && ( return task?.instance?.length > 0 && (
<> <>
<Steps <Steps
progressDot progressDot
direction="vertical" direction="vertical"
items={Object.values(task?.instance).map((item) => ({ items={Object.values(task?.instance).map((item) => ({
title: <Typography.Link title: <Typography.Link
onClick={() => navigate(`/data/operator-market/plugin-detail/${item?.id}`)} onClick={() => navigate(`/data/operator-market/plugin-detail/${item?.id}`)}
> >
{item?.name} {item?.name}
</Typography.Link>, </Typography.Link>,
description: item?.description, description: item?.description,
status: "finish" status: "finish"
}))} }))}
className="overflow-auto" className="overflow-auto"
/> />
</> </>
); );
} }

View File

@@ -1,61 +1,61 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Tabs, Button } from "antd"; import { Tabs, Button } from "antd";
import { PlusOutlined } from "@ant-design/icons"; import { PlusOutlined } from "@ant-design/icons";
import { useNavigate } from "react-router"; import { useNavigate } from "react-router";
import TaskList from "./components/TaskList"; import TaskList from "./components/TaskList";
import TemplateList from "./components/TemplateList"; import TemplateList from "./components/TemplateList";
import ProcessFlowDiagram from "./components/ProcessFlowDiagram"; import ProcessFlowDiagram from "./components/ProcessFlowDiagram";
import { useSearchParams } from "@/hooks/useSearchParams"; import { useSearchParams } from "@/hooks/useSearchParams";
export default function DataProcessingPage() { export default function DataProcessingPage() {
const navigate = useNavigate(); const navigate = useNavigate();
const urlParams = useSearchParams(); const urlParams = useSearchParams();
const [currentView, setCurrentView] = useState<"task" | "template">("task"); const [currentView, setCurrentView] = useState<"task" | "template">("task");
useEffect(() => { useEffect(() => {
if (urlParams.view) { if (urlParams.view) {
setCurrentView(urlParams.view); setCurrentView(urlParams.view);
} }
}, [urlParams]); }, [urlParams]);
return ( return (
<div className="h-full flex flex-col gap-4"> <div className="h-full flex flex-col gap-4">
{/* Header */} {/* Header */}
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<h1 className="text-xl font-bold"></h1> <h1 className="text-xl font-bold"></h1>
<div className="flex gap-2"> <div className="flex gap-2">
<Button <Button
icon={<PlusOutlined />} icon={<PlusOutlined />}
onClick={() => navigate("/data/cleansing/create-template")} onClick={() => navigate("/data/cleansing/create-template")}
> >
</Button> </Button>
<Button <Button
type="primary" type="primary"
icon={<PlusOutlined />} icon={<PlusOutlined />}
onClick={() => navigate("/data/cleansing/create-task")} onClick={() => navigate("/data/cleansing/create-task")}
> >
</Button> </Button>
</div> </div>
</div> </div>
<ProcessFlowDiagram /> <ProcessFlowDiagram />
<Tabs <Tabs
activeKey={currentView} activeKey={currentView}
onChange={(key) => setCurrentView(key as any)} onChange={(key) => setCurrentView(key as any)}
items={[ items={[
{ {
key: "task", key: "task",
label: "任务列表", label: "任务列表",
}, },
{ {
key: "template", key: "template",
label: "模板管理", label: "模板管理",
}, },
]} ]}
/> />
{currentView === "task" && <TaskList />} {currentView === "task" && <TaskList />}
{currentView === "template" && <TemplateList />} {currentView === "template" && <TemplateList />}
</div> </div>
); );
} }

View File

@@ -1,86 +1,86 @@
import { import {
ArrowRight, ArrowRight,
CheckCircle, CheckCircle,
Database, Database,
Play, Play,
Settings, Settings,
Workflow, Workflow,
Zap, Zap,
} from "lucide-react"; } from "lucide-react";
// 流程图组件 // 流程图组件
export default function ProcessFlowDiagram() { export default function ProcessFlowDiagram() {
const flowSteps = [ const flowSteps = [
{ {
id: "start", id: "start",
label: "开始", label: "开始",
type: "start", type: "start",
icon: Play, icon: Play,
color: "bg-green-500", color: "bg-green-500",
}, },
{ {
id: "select", id: "select",
label: "选择数据集", label: "选择数据集",
type: "process", type: "process",
icon: Database, icon: Database,
color: "bg-blue-500", color: "bg-blue-500",
}, },
{ {
id: "config", id: "config",
label: "基本配置", label: "基本配置",
type: "process", type: "process",
icon: Settings, icon: Settings,
color: "bg-purple-500", color: "bg-purple-500",
}, },
{ {
id: "operators", id: "operators",
label: "算子编排", label: "算子编排",
type: "process", type: "process",
icon: Workflow, icon: Workflow,
color: "bg-orange-500", color: "bg-orange-500",
}, },
{ {
id: "execute", id: "execute",
label: "执行任务", label: "执行任务",
type: "process", type: "process",
icon: Zap, icon: Zap,
color: "bg-red-500", color: "bg-red-500",
}, },
{ {
id: "end", id: "end",
label: "完成", label: "完成",
type: "end", type: "end",
icon: CheckCircle, icon: CheckCircle,
color: "bg-green-500", color: "bg-green-500",
}, },
]; ];
return ( return (
<div className="border-card p-6"> <div className="border-card p-6">
<div className="w-full flex items-center justify-center"> <div className="w-full flex items-center justify-center">
<div className="w-full flex items-center space-x-12"> <div className="w-full flex items-center space-x-12">
{flowSteps.map((step, index) => { {flowSteps.map((step, index) => {
const IconComponent = step.icon; const IconComponent = step.icon;
return ( return (
<div key={step.id} className="flex-1 flex items-center"> <div key={step.id} className="flex-1 flex items-center">
<div className="flex flex-col items-center w-full"> <div className="flex flex-col items-center w-full">
<div <div
className={`w-12 h-12 ${step.color} rounded-full flex items-center justify-center text-white shadow-lg`} className={`w-12 h-12 ${step.color} rounded-full flex items-center justify-center text-white shadow-lg`}
> >
<IconComponent className="w-6 h-6" /> <IconComponent className="w-6 h-6" />
</div> </div>
<span className="text-xs font-medium text-gray-700 mt-2 text-center max-w-16"> <span className="text-xs font-medium text-gray-700 mt-2 text-center max-w-16">
{step.label} {step.label}
</span> </span>
</div> </div>
{index < flowSteps.length - 1 && ( {index < flowSteps.length - 1 && (
<ArrowRight className="w-6 h-6 text-gray-400 mx-3" /> <ArrowRight className="w-6 h-6 text-gray-400 mx-3" />
)} )}
</div> </div>
); );
})} })}
</div> </div>
</div> </div>
</div> </div>
); );
} }

View File

@@ -1,308 +1,308 @@
import { useState } from "react"; import { useState } from "react";
import { Table, Progress, Badge, Button, Tooltip, Card, App } from "antd"; import { Table, Progress, Badge, Button, Tooltip, Card, App } from "antd";
import { import {
PlayCircleOutlined, PlayCircleOutlined,
PauseCircleOutlined, PauseCircleOutlined,
DeleteOutlined, DeleteOutlined,
} from "@ant-design/icons"; } from "@ant-design/icons";
import { SearchControls } from "@/components/SearchControls"; import { SearchControls } from "@/components/SearchControls";
import CardView from "@/components/CardView"; import CardView from "@/components/CardView";
import { useNavigate } from "react-router"; import { useNavigate } from "react-router";
import { mapTask, TaskStatusMap } from "../../cleansing.const"; import { mapTask, TaskStatusMap } from "../../cleansing.const";
import { import {
TaskStatus, TaskStatus,
type CleansingTask, type CleansingTask,
} from "@/pages/DataCleansing/cleansing.model"; } from "@/pages/DataCleansing/cleansing.model";
import useFetchData from "@/hooks/useFetchData"; import useFetchData from "@/hooks/useFetchData";
import { import {
deleteCleaningTaskByIdUsingDelete, deleteCleaningTaskByIdUsingDelete,
executeCleaningTaskUsingPost, executeCleaningTaskUsingPost,
queryCleaningTasksUsingGet, queryCleaningTasksUsingGet,
stopCleaningTaskUsingPost, stopCleaningTaskUsingPost,
} from "../../cleansing.api"; } from "../../cleansing.api";
export default function TaskList() { export default function TaskList() {
const navigate = useNavigate(); const navigate = useNavigate();
const { message } = App.useApp(); const { message } = App.useApp();
const [viewMode, setViewMode] = useState<"card" | "list">("list"); const [viewMode, setViewMode] = useState<"card" | "list">("list");
const filterOptions = [ const filterOptions = [
{ {
key: "status", key: "status",
label: "状态", label: "状态",
options: [...Object.values(TaskStatusMap)], options: [...Object.values(TaskStatusMap)],
}, },
]; ];
const { const {
loading, loading,
tableData, tableData,
pagination, pagination,
searchParams, searchParams,
setSearchParams, setSearchParams,
fetchData, fetchData,
handleFiltersChange, handleFiltersChange,
handleKeywordChange, handleKeywordChange,
} = useFetchData(queryCleaningTasksUsingGet, mapTask); } = useFetchData(queryCleaningTasksUsingGet, mapTask);
const pauseTask = async (item: CleansingTask) => { const pauseTask = async (item: CleansingTask) => {
await stopCleaningTaskUsingPost(item.id); await stopCleaningTaskUsingPost(item.id);
message.success("任务已暂停"); message.success("任务已暂停");
fetchData(); fetchData();
}; };
const startTask = async (item: CleansingTask) => { const startTask = async (item: CleansingTask) => {
await executeCleaningTaskUsingPost(item.id); await executeCleaningTaskUsingPost(item.id);
message.success("任务已启动"); message.success("任务已启动");
fetchData(); fetchData();
}; };
const deleteTask = async (item: CleansingTask) => { const deleteTask = async (item: CleansingTask) => {
await deleteCleaningTaskByIdUsingDelete(item.id); await deleteCleaningTaskByIdUsingDelete(item.id);
message.success("任务已删除"); message.success("任务已删除");
fetchData(); fetchData();
}; };
const taskOperations = (record: CleansingTask) => { const taskOperations = (record: CleansingTask) => {
const isRunning = record.status?.value === TaskStatus.RUNNING; const isRunning = record.status?.value === TaskStatus.RUNNING;
const showStart = [ const showStart = [
TaskStatus.PENDING, TaskStatus.PENDING,
TaskStatus.FAILED, TaskStatus.FAILED,
TaskStatus.STOPPED, TaskStatus.STOPPED,
].includes(record.status?.value); ].includes(record.status?.value);
const pauseBtn = { const pauseBtn = {
key: "pause", key: "pause",
label: "暂停", label: "暂停",
icon: isRunning ? <PauseCircleOutlined /> : <PlayCircleOutlined />, icon: isRunning ? <PauseCircleOutlined /> : <PlayCircleOutlined />,
onClick: pauseTask, // implement pause/play logic onClick: pauseTask, // implement pause/play logic
}; };
const startBtn = { const startBtn = {
key: "start", key: "start",
label: "启动", label: "启动",
icon: isRunning ? <PauseCircleOutlined /> : <PlayCircleOutlined />, icon: isRunning ? <PauseCircleOutlined /> : <PlayCircleOutlined />,
onClick: startTask, // implement pause/play logic onClick: startTask, // implement pause/play logic
}; };
return [ return [
...(isRunning ...(isRunning
? [ pauseBtn ] ? [ pauseBtn ]
: []), : []),
...(showStart ...(showStart
? [ startBtn ] ? [ startBtn ]
: []), : []),
{ {
key: "delete", key: "delete",
label: "删除", label: "删除",
danger: true, danger: true,
icon: <DeleteOutlined />, icon: <DeleteOutlined />,
onClick: deleteTask, // implement delete logic onClick: deleteTask, // implement delete logic
}, },
]; ];
}; };
const taskColumns = [ const taskColumns = [
{ {
title: "任务名称", title: "任务名称",
dataIndex: "name", dataIndex: "name",
key: "name", key: "name",
fixed: "left", fixed: "left",
width: 150, width: 150,
ellipsis: true, ellipsis: true,
render: (_, task: CleansingTask) => { render: (_, task: CleansingTask) => {
return ( return (
<Button <Button
type="link" type="link"
onClick={() => onClick={() =>
navigate("/data/cleansing/task-detail/" + task.id) navigate("/data/cleansing/task-detail/" + task.id)
} }
> >
{task.name} {task.name}
</Button> </Button>
); );
}, },
}, },
{ {
title: "任务ID", title: "任务ID",
dataIndex: "id", dataIndex: "id",
key: "id", key: "id",
width: 150, width: 150,
ellipsis: true, ellipsis: true,
}, },
{ {
title: "源数据集", title: "源数据集",
dataIndex: "srcDatasetId", dataIndex: "srcDatasetId",
key: "srcDatasetId", key: "srcDatasetId",
width: 150, width: 150,
ellipsis: true, ellipsis: true,
render: (_, record: CleansingTask) => { render: (_, record: CleansingTask) => {
return ( return (
<Button <Button
type="link" type="link"
onClick={() => onClick={() =>
navigate("/data/management/detail/" + record.srcDatasetId) navigate("/data/management/detail/" + record.srcDatasetId)
} }
> >
{record.srcDatasetName} {record.srcDatasetName}
</Button> </Button>
); );
}, },
}, },
{ {
title: "目标数据集", title: "目标数据集",
dataIndex: "destDatasetId", dataIndex: "destDatasetId",
key: "destDatasetId", key: "destDatasetId",
width: 150, width: 150,
ellipsis: true, ellipsis: true,
render: (_, record: CleansingTask) => { render: (_, record: CleansingTask) => {
return ( return (
<Button <Button
type="link" type="link"
onClick={() => onClick={() =>
navigate("/data/management/detail/" + record.destDatasetId) navigate("/data/management/detail/" + record.destDatasetId)
} }
> >
{record.destDatasetName} {record.destDatasetName}
</Button> </Button>
); );
}, },
}, },
{ {
title: "状态", title: "状态",
dataIndex: "status", dataIndex: "status",
key: "status", key: "status",
width: 100, width: 100,
render: (status: any) => { render: (status: any) => {
return <Badge color={status?.color} text={status?.label} />; return <Badge color={status?.color} text={status?.label} />;
}, },
}, },
{ {
title: "进度", title: "进度",
dataIndex: "process", dataIndex: "process",
key: "process", key: "process",
width: 150, width: 150,
render: (_, record: CleansingTask) => { render: (_, record: CleansingTask) => {
if (record?.status?.value == TaskStatus.FAILED) { if (record?.status?.value == TaskStatus.FAILED) {
return <Progress percent={record?.progress?.process} size="small" status="exception" />; return <Progress percent={record?.progress?.process} size="small" status="exception" />;
} }
return <Progress percent={record?.progress?.process} size="small"/>; return <Progress percent={record?.progress?.process} size="small"/>;
}, },
}, },
{ {
title: "已处理文件数", title: "已处理文件数",
dataIndex: "finishedFileNum", dataIndex: "finishedFileNum",
key: "finishedFileNum", key: "finishedFileNum",
width: 120, width: 120,
align: "right", align: "right",
ellipsis: true, ellipsis: true,
}, },
{ {
title: "总文件数", title: "总文件数",
dataIndex: "totalFileNum", dataIndex: "totalFileNum",
key: "totalFileNum", key: "totalFileNum",
width: 100, width: 100,
align: "right", align: "right",
ellipsis: true, ellipsis: true,
}, },
{ {
title: "执行耗时", title: "执行耗时",
dataIndex: "duration", dataIndex: "duration",
key: "duration", key: "duration",
width: 100, width: 100,
ellipsis: true, ellipsis: true,
}, },
{ {
title: "开始时间", title: "开始时间",
dataIndex: "startedAt", dataIndex: "startedAt",
key: "startedAt", key: "startedAt",
width: 180, width: 180,
ellipsis: true, ellipsis: true,
}, },
{ {
title: "结束时间", title: "结束时间",
dataIndex: "finishedAt", dataIndex: "finishedAt",
key: "finishedAt", key: "finishedAt",
width: 180, width: 180,
ellipsis: true, ellipsis: true,
}, },
{ {
title: "创建时间", title: "创建时间",
dataIndex: "createdAt", dataIndex: "createdAt",
key: "createdAt", key: "createdAt",
width: 180, width: 180,
ellipsis: true, ellipsis: true,
}, },
{ {
title: "数据量变化", title: "数据量变化",
dataIndex: "dataSizeChange", dataIndex: "dataSizeChange",
key: "dataSizeChange", key: "dataSizeChange",
width: 180, width: 180,
ellipsis: true, ellipsis: true,
render: (_: any, record: CleansingTask) => { render: (_: any, record: CleansingTask) => {
if (record.before !== undefined && record.after !== undefined) { if (record.before !== undefined && record.after !== undefined) {
return `${record.before}${record.after}`; return `${record.before}${record.after}`;
} }
return "-"; return "-";
}, },
}, },
{ {
title: "操作", title: "操作",
key: "action", key: "action",
fixed: "right", fixed: "right",
render: (text: string, record: any) => ( render: (text: string, record: any) => (
<div className="flex gap-2"> <div className="flex gap-2">
{taskOperations(record).map((op) => {taskOperations(record).map((op) =>
op ? ( op ? (
<Tooltip key={op.key} title={op.label}> <Tooltip key={op.key} title={op.label}>
<Button <Button
type="text" type="text"
icon={op.icon} icon={op.icon}
danger={op?.danger} danger={op?.danger}
onClick={() => op.onClick(record)} onClick={() => op.onClick(record)}
/> />
</Tooltip> </Tooltip>
) : null ) : null
)} )}
</div> </div>
), ),
}, },
]; ];
return ( return (
<> <>
{/* Search and Filters */} {/* Search and Filters */}
<SearchControls <SearchControls
searchTerm={searchParams.keyword} searchTerm={searchParams.keyword}
onSearchChange={handleKeywordChange} onSearchChange={handleKeywordChange}
searchPlaceholder="搜索任务名称、描述" searchPlaceholder="搜索任务名称、描述"
filters={filterOptions} filters={filterOptions}
onFiltersChange={handleFiltersChange} onFiltersChange={handleFiltersChange}
viewMode={viewMode} viewMode={viewMode}
onViewModeChange={setViewMode} onViewModeChange={setViewMode}
showViewToggle={true} showViewToggle={true}
onReload={fetchData} onReload={fetchData}
onClearFilters={() => setSearchParams({ ...searchParams, filter: {} })} onClearFilters={() => setSearchParams({ ...searchParams, filter: {} })}
/> />
{/* Task List */} {/* Task List */}
{viewMode === "card" ? ( {viewMode === "card" ? (
<CardView <CardView
data={tableData} data={tableData}
operations={taskOperations} operations={taskOperations}
pagination={pagination} pagination={pagination}
onView={(tableData) => { onView={(tableData) => {
navigate("/data/cleansing/task-detail/" + tableData.id) navigate("/data/cleansing/task-detail/" + tableData.id)
}} }}
/> />
) : ( ) : (
<Card> <Card>
<Table <Table
columns={taskColumns} columns={taskColumns}
dataSource={tableData} dataSource={tableData}
rowKey="id" rowKey="id"
loading={loading} loading={loading}
scroll={{ x: "max-content", y: "calc(100vh - 35rem)" }} scroll={{ x: "max-content", y: "calc(100vh - 35rem)" }}
pagination={pagination} pagination={pagination}
/> />
</Card> </Card>
)} )}
</> </>
); );
} }

View File

@@ -1,156 +1,156 @@
import {DeleteOutlined, EditOutlined} from "@ant-design/icons"; import {DeleteOutlined, EditOutlined} from "@ant-design/icons";
import CardView from "@/components/CardView"; import CardView from "@/components/CardView";
import { import {
deleteCleaningTemplateByIdUsingDelete, queryCleaningTemplatesUsingGet, deleteCleaningTemplateByIdUsingDelete, queryCleaningTemplatesUsingGet,
} from "../../cleansing.api"; } from "../../cleansing.api";
import useFetchData from "@/hooks/useFetchData"; import useFetchData from "@/hooks/useFetchData";
import {mapTemplate} from "../../cleansing.const"; import {mapTemplate} from "../../cleansing.const";
import {App, Button, Card, Table, Tooltip} from "antd"; import {App, Button, Card, Table, Tooltip} from "antd";
import {CleansingTemplate} from "../../cleansing.model"; import {CleansingTemplate} from "../../cleansing.model";
import {SearchControls} from "@/components/SearchControls.tsx"; import {SearchControls} from "@/components/SearchControls.tsx";
import {useNavigate} from "react-router"; import {useNavigate} from "react-router";
import {useState} from "react"; import {useState} from "react";
export default function TemplateList() { export default function TemplateList() {
const navigate = useNavigate(); const navigate = useNavigate();
const { message } = App.useApp(); const { message } = App.useApp();
const [viewMode, setViewMode] = useState<"card" | "list">("list"); const [viewMode, setViewMode] = useState<"card" | "list">("list");
const { const {
loading, loading,
tableData, tableData,
pagination, pagination,
searchParams, searchParams,
setSearchParams, setSearchParams,
fetchData, fetchData,
handleFiltersChange, handleFiltersChange,
handleKeywordChange, handleKeywordChange,
} = useFetchData(queryCleaningTemplatesUsingGet, mapTemplate); } = useFetchData(queryCleaningTemplatesUsingGet, mapTemplate);
const templateOperations = () => { const templateOperations = () => {
return [ return [
{ {
key: "update", key: "update",
label: "编辑", label: "编辑",
icon: <EditOutlined />, icon: <EditOutlined />,
onClick: (template: CleansingTemplate) => navigate(`/data/cleansing/update-template/${template.id}`) onClick: (template: CleansingTemplate) => navigate(`/data/cleansing/update-template/${template.id}`)
}, },
{ {
key: "delete", key: "delete",
label: "删除", label: "删除",
danger: true, danger: true,
icon: <DeleteOutlined />, icon: <DeleteOutlined />,
onClick: deleteTemplate, // implement delete logic onClick: deleteTemplate, // implement delete logic
}, },
]; ];
}; };
const templateColumns = [ const templateColumns = [
{ {
title: "模板名称", title: "模板名称",
dataIndex: "name", dataIndex: "name",
key: "name", key: "name",
fixed: "left", fixed: "left",
width: 150, width: 150,
ellipsis: true, ellipsis: true,
render: (_, template: CleansingTemplate) => { render: (_, template: CleansingTemplate) => {
return ( return (
<Button <Button
type="link" type="link"
onClick={() => onClick={() =>
navigate("/data/cleansing/template-detail/" + template.id) navigate("/data/cleansing/template-detail/" + template.id)
} }
> >
{template.name} {template.name}
</Button> </Button>
); );
}}, }},
{ {
title: "模板ID", title: "模板ID",
dataIndex: "id", dataIndex: "id",
key: "id", key: "id",
fixed: "left", fixed: "left",
width: 150, width: 150,
}, },
{ {
title: "算子数量", title: "算子数量",
dataIndex: "num", dataIndex: "num",
key: "num", key: "num",
width: 100, width: 100,
ellipsis: true, ellipsis: true,
render: (_, template: CleansingTemplate) => { render: (_, template: CleansingTemplate) => {
return template.instance?.length ?? 0; return template.instance?.length ?? 0;
}, },
}, },
{ {
title: "操作", title: "操作",
key: "action", key: "action",
fixed: "right", fixed: "right",
width: 20, width: 20,
render: (text: string, record: any) => ( render: (text: string, record: any) => (
<div className="flex gap-2"> <div className="flex gap-2">
{templateOperations(record).map((op) => {templateOperations(record).map((op) =>
op ? ( op ? (
<Tooltip key={op.key} title={op.label}> <Tooltip key={op.key} title={op.label}>
<Button <Button
type="text" type="text"
icon={op.icon} icon={op.icon}
danger={op?.danger} danger={op?.danger}
onClick={() => op.onClick(record)} onClick={() => op.onClick(record)}
/> />
</Tooltip> </Tooltip>
) : null ) : null
)} )}
</div> </div>
), ),
}, },
] ]
const deleteTemplate = async (template: CleansingTemplate) => { const deleteTemplate = async (template: CleansingTemplate) => {
if (!template.id) { if (!template.id) {
return; return;
} }
// 实现删除逻辑 // 实现删除逻辑
await deleteCleaningTemplateByIdUsingDelete(template.id); await deleteCleaningTemplateByIdUsingDelete(template.id);
fetchData(); fetchData();
message.success("模板删除成功"); message.success("模板删除成功");
}; };
return ( return (
<> <>
{/* Search and Filters */} {/* Search and Filters */}
<SearchControls <SearchControls
searchTerm={searchParams.keyword} searchTerm={searchParams.keyword}
onSearchChange={handleKeywordChange} onSearchChange={handleKeywordChange}
searchPlaceholder="搜索模板名称、描述" searchPlaceholder="搜索模板名称、描述"
onFiltersChange={handleFiltersChange} onFiltersChange={handleFiltersChange}
viewMode={viewMode} viewMode={viewMode}
onViewModeChange={setViewMode} onViewModeChange={setViewMode}
showViewToggle={true} showViewToggle={true}
onReload={fetchData} onReload={fetchData}
onClearFilters={() => setSearchParams({ ...searchParams, filter: {} })} onClearFilters={() => setSearchParams({ ...searchParams, filter: {} })}
/> />
{viewMode === "card" ? ( {viewMode === "card" ? (
<CardView <CardView
data={tableData} data={tableData}
operations={templateOperations} operations={templateOperations}
pagination={pagination} pagination={pagination}
onView={(tableData) => { onView={(tableData) => {
navigate("/data/cleansing/template-detail/" + tableData.id) navigate("/data/cleansing/template-detail/" + tableData.id)
}} }}
/> />
) : ( ) : (
<Card> <Card>
<Table <Table
columns={templateColumns} columns={templateColumns}
dataSource={tableData} dataSource={tableData}
rowKey="id" rowKey="id"
loading={loading} loading={loading}
scroll={{ x: "max-content", y: "calc(100vh - 35rem)" }} scroll={{ x: "max-content", y: "calc(100vh - 35rem)" }}
pagination={pagination} pagination={pagination}
/> />
</Card> </Card>
)} )}
</> </>
); );
} }

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