You've already forked DataMate
Revert "feat: fix the problem in the Operator Market frontend pages"
This commit is contained in:
@@ -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
54
frontend/.gitignore
vendored
@@ -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
|
||||||
@@ -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根配置
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|||||||
@@ -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
14450
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
}
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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" />,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
@@ -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,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -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: [],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
@@ -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,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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
@@ -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
@@ -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" });
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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
@@ -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" });
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,457 +1,457 @@
|
|||||||
import { useState } from "react";
|
import { 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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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"><{item.value}></Tag>
|
<Tag color="blue"><{item.value}></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"><{item.value}></Tag>
|
<Tag color="green"><{item.value}></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 }}
|
||||||
>
|
>
|
||||||
需要 <{ctrlConfig.child_tag}>
|
需要 <{ctrlConfig.child_tag}>
|
||||||
</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;
|
||||||
|
|||||||
@@ -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">需要 <{config.child_tag}></Tag>
|
<Tag color="green">需要 <{config.child_tag}></Tag>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Space>
|
</Space>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
Reference in New Issue
Block a user