Compare commits

..

1 Commits

Author SHA1 Message Date
yue02.sun
1331552cfc 提交代码 2025-08-05 15:30:03 +08:00
8 changed files with 225 additions and 121 deletions

View File

@ -15,6 +15,7 @@
"axios": "^1.11.0", "axios": "^1.11.0",
"i18next": "^25.3.2", "i18next": "^25.3.2",
"i18next-browser-languagedetector": "^8.2.0", "i18next-browser-languagedetector": "^8.2.0",
"lodash": "^4.17.21",
"react": "^19.1.0", "react": "^19.1.0",
"react-dom": "^19.1.0", "react-dom": "^19.1.0",
"react-i18next": "^15.6.0", "react-i18next": "^15.6.0",
@ -23,6 +24,7 @@
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.30.1", "@eslint/js": "^9.30.1",
"@types/lodash": "^4.17.20",
"@types/node": "^24.0.14", "@types/node": "^24.0.14",
"@types/react": "^19.1.8", "@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6", "@types/react-dom": "^19.1.6",

View File

@ -23,18 +23,21 @@ export const dataGenerationApi = {
return request.post(`${baseUrl}`, params); return request.post(`${baseUrl}`, params);
}, },
stopTask: (params: { stopTask: (params: {
task_id: number; task_id: number | string;
}) => { }) => {
return request.post(`${baseUrl}/${params.task_id}/stop`); return request.post(`${baseUrl}/${params.task_id}/stop`);
}, },
deleteTask: (params: { deleteTask: (params: {
task_id: number; task_id: number | string;
}) => { }) => {
return request.delete(`${baseUrl}/${params.task_id}`); return request.delete(`${baseUrl}/${params.task_id}`);
}, },
getLogs: (params: { getLogs: (params: {
task_id: number; task_id: number | string;
}) => { }) => {
return request.get(`${baseUrl}/${params.task_id}/logs`); return request.get(`${baseUrl}/${params.task_id}/logs`);
}, },
getOptions: () => {
return request.get(`${baseUrl}/options`);
},
}; };

View File

@ -13,7 +13,7 @@ export const datasetApi = {
}, },
createDataset: (params: { createDataset: (params: {
name: string; name: string;
description: string; description?: string;
}) => { }) => {
return request.post(`${baseUrl}`, params); return request.post(`${baseUrl}`, params);
}, },

View File

@ -1,3 +1,4 @@
import { datasetApi } from "@/api/dataset";
import { import {
Button, Button,
Divider, Divider,
@ -5,6 +6,7 @@ import {
Form, Form,
Input, Input,
InputNumber, InputNumber,
message,
Modal, Modal,
Select, Select,
Space, Space,
@ -12,11 +14,18 @@ import {
} from "antd"; } from "antd";
import React, { useEffect } from "react"; import React, { useEffect } from "react";
// 生成简短的UUID
const generateShortUUID = () => {
return Math.random().toString(36).substring(2, 8);
};
interface CreateTaskModalProps { interface CreateTaskModalProps {
visible: boolean; visible: boolean;
onCancel: () => void; onCancel: () => void;
onConfirm: (values: any) => void; // 修改onConfirm的类型以接收表单值 onConfirm: (values: any) => void; // 修改onConfirm的类型以接收表单值
datasetList: { id: number; name: string; description: string }[] | undefined; datasetList: { id: number; name: string; description: string }[] | undefined;
taskTypeOptions?: { value: string; label: string }[];
onDatasetCreate?: () => void;
} }
const CreateTaskModal: React.FC<CreateTaskModalProps> = ({ const CreateTaskModal: React.FC<CreateTaskModalProps> = ({
@ -24,11 +33,45 @@ const CreateTaskModal: React.FC<CreateTaskModalProps> = ({
onCancel, onCancel,
onConfirm, onConfirm,
datasetList, datasetList,
taskTypeOptions,
onDatasetCreate
}) => { }) => {
const [form] = Form.useForm(); const [form] = Form.useForm();
const [datasetName, setDatasetName] = React.useState(""); const [datasetName, setDatasetName] = React.useState("");
const [loading, setLoading] = React.useState(false); const [loading, setLoading] = React.useState(false);
// 监听表单字段变化,自动生成数据集名称
useEffect(() => {
const simulator = form.getFieldValue('simulator');
const taskType = form.getFieldValue('task_type');
const camera = form.getFieldValue('camera');
if (simulator && taskType && camera) {
const uuid = generateShortUUID();
const generatedName = `${simulator}-${taskType}-${camera}-${uuid}`;
setDatasetName(generatedName);
}
}, [form]);
// 监听表单字段变化
const handleFormChange = () => {
const taskType = form.getFieldValue('task_type');
const camera = form.getFieldValue('camera');
if (taskType && camera) {
const uuid = generateShortUUID();
const generatedName = `${taskType}-${camera}-${uuid}`;
setDatasetName(generatedName);
}
};
const simulationOptions = [
{ value: "robotwin", label: "robotwin" },
];
const cameraTypeOptions = [
{ value: "D435", label: "D435" },
{ value: "D515", label: "D515" },
];
const handleOk = async () => { const handleOk = async () => {
try { try {
const values = await form.validateFields(); const values = await form.validateFields();
@ -41,7 +84,18 @@ const CreateTaskModal: React.FC<CreateTaskModalProps> = ({
const handleCreateDataset = async () => { const handleCreateDataset = async () => {
setLoading(true); setLoading(true);
try {
await datasetApi.createDataset({
name: datasetName,
});
message.success("创建数据集成功");
setLoading(false);
setDatasetName("");
onDatasetCreate?.();
} catch (error) {
setLoading(false);
message.error("创建数据集失败");
}
}; };
return ( return (
@ -50,21 +104,16 @@ const CreateTaskModal: React.FC<CreateTaskModalProps> = ({
closable={false} closable={false}
open={visible} open={visible}
onCancel={onCancel} onCancel={onCancel}
onOk={handleOk} // 修改为handleOk onOk={handleOk}
destroyOnHidden destroyOnHidden
> >
<Form form={form} layout="vertical"> <Form form={form} layout="vertical" onValuesChange={handleFormChange}>
<Form.Item label="任务名称" name="task_name">
<Input placeholder="请输入任务名称(可选)" />
</Form.Item>
<Form.Item <Form.Item
label="仿真器" label="仿真器"
name="simulator" name="simulator"
rules={[{ required: true, message: "请选择仿真器!" }]} rules={[{ required: true, message: "请选择仿真器!" }]}
> >
<Select placeholder="请选择仿真器"> <Select placeholder="请选择仿真器" options={simulationOptions}>
<Select.Option value="robotwin">robotwin</Select.Option>
{/* 可以根据实际情况添加更多仿真器选项 */}
</Select> </Select>
</Form.Item> </Form.Item>
<Form.Item <Form.Item
@ -72,11 +121,7 @@ const CreateTaskModal: React.FC<CreateTaskModalProps> = ({
name="task_type" name="task_type"
rules={[{ required: true, message: "请选择任务类型!" }]} rules={[{ required: true, message: "请选择任务类型!" }]}
> >
<Select placeholder="请选择任务类型"> <Select placeholder="请选择任务类型" options={taskTypeOptions}>
<Select.Option value="beat_block_hammer">
beat_block_hammer
</Select.Option>
{/* 可以根据实际情况添加更多任务类型选项 */}
</Select> </Select>
</Form.Item> </Form.Item>
<Form.Item <Form.Item
@ -84,9 +129,7 @@ const CreateTaskModal: React.FC<CreateTaskModalProps> = ({
name="camera" name="camera"
rules={[{ required: true, message: "请选择相机类型!" }]} rules={[{ required: true, message: "请选择相机类型!" }]}
> >
<Select placeholder="请选择相机类型"> <Select placeholder="请选择相机类型" options={cameraTypeOptions}>
<Select.Option value="D435">D435</Select.Option>
{/* 可以根据实际情况添加更多相机类型选项 */}
</Select> </Select>
</Form.Item> </Form.Item>
<Form.Item <Form.Item
@ -101,6 +144,9 @@ const CreateTaskModal: React.FC<CreateTaskModalProps> = ({
placeholder="请输入轨迹条数 (1-10000)" placeholder="请输入轨迹条数 (1-10000)"
/> />
</Form.Item> </Form.Item>
<Form.Item label="任务名称" name="task_name">
<Input placeholder="请输入任务名称(可选)" />
</Form.Item>
<Form.Item <Form.Item
label="数据集" label="数据集"
name="dataset_id" name="dataset_id"

View File

@ -1,5 +1,5 @@
import ColLayout from "@/components/ColLayout"; import ColLayout from "@/components/ColLayout";
import { Button, Flex, Pagination, Table } from "antd"; import { Button, Flex, message, Pagination, Popconfirm, Table } from "antd";
import { createStyles } from "antd-style"; import { createStyles } from "antd-style";
import ButtonGroup from "antd/es/button/button-group"; import ButtonGroup from "antd/es/button/button-group";
import Search from "antd/es/input/Search"; import Search from "antd/es/input/Search";
@ -9,6 +9,8 @@ import { datasetApi } from "@/api/dataset";
import useToken from "antd/es/theme/useToken"; import useToken from "antd/es/theme/useToken";
import { dataGenerationApi } from "@/api/data-generation"; import { dataGenerationApi } from "@/api/data-generation";
import { preciseInterval } from "@/utils"; import { preciseInterval } from "@/utils";
import dayjs from "dayjs";
import { debounce } from "lodash";
// 假设的任务类型接口,如果已存在请忽略 // 假设的任务类型接口,如果已存在请忽略
interface TaskItem { interface TaskItem {
@ -17,7 +19,7 @@ interface TaskItem {
type: string; type: string;
create_time: string; create_time: string;
update_time: string; update_time: string;
// 其他可能的字段 id: number | string;
} }
interface DataGenerationPageProps {} interface DataGenerationPageProps {}
@ -37,6 +39,10 @@ const DataGenerationPage: React.FC<DataGenerationPageProps> = () => {
const [, token] = useToken(); const [, token] = useToken();
const PollyRef = useRef<any>(null); const PollyRef = useRef<any>(null);
const refreshTime = 5000; const refreshTime = 5000;
const [taskTypeOptions, setTaskTypeOptions] = useState<
{ value: string; label: string }[]
>([]);
const [searchKeyword, setSearchKeyword] = useState<string>("");
const HeaderSlot = ( const HeaderSlot = (
<div> <div>
@ -66,7 +72,7 @@ const DataGenerationPage: React.FC<DataGenerationPageProps> = () => {
const columns = [ const columns = [
{ {
title: "任务名称", title: "任务名称",
dataIndex: "name", dataIndex: "task_name",
key: "name", key: "name",
}, },
{ {
@ -76,78 +82,126 @@ const DataGenerationPage: React.FC<DataGenerationPageProps> = () => {
}, },
{ {
title: "任务类型", title: "任务类型",
dataIndex: "type", dataIndex: "task_type",
key: "type", key: "type",
}, },
{ {
title: "任务创建时间", title: "任务创建时间",
dataIndex: "create_time", dataIndex: "created_at",
key: "create_time", key: "created_at",
render: (text: string) => dayjs(text).format("YYYY-MM-DD HH:mm:ss"),
}, },
{ {
title: "任务更新时间", title: "任务更新时间",
dataIndex: "update_time", dataIndex: "updated_at",
key: "update_time", key: "updated_at",
render: (text: string) => dayjs(text).format("YYYY-MM-DD HH:mm:ss"),
}, },
{ {
title: "操作", title: "操作",
key: "action", key: "action",
render: (_: any, record: TaskItem) => ( render: (_: any, record: TaskItem) => (
<ButtonGroup> <span>
{record.status === "running" ? ( // 假设 "running" 表示执行中 {record.status === "running" ? ( // 假设 "running" 表示执行中
<> <Flex gap={16}>
<Button type="primary"></Button> <Popconfirm
title="确定停止吗?"
onConfirm={() => onOperate?.("stop", record)}
>
<Button type="primary" danger>
</Button>
</Popconfirm>
<Button type="primary"></Button> <Button type="primary"></Button>
</> </Flex>
) : ( ) : (
<> <Flex gap={16}>
<Button type="primary"></Button> <Button type="primary" danger></Button>
<Button type="primary"></Button> <Button type="primary"></Button>
</> </Flex>
)} )}
</ButtonGroup> </span>
), ),
}, },
]; ];
const getDatasetList = async ( const onOperate = async (type: "stop" | "delete", record: TaskItem) => {
page: number, try {
pageSize: number, if (type === "stop") {
) => { await dataGenerationApi.stopTask({ task_id: record.id });
const { data } = await datasetApi.getDatasetList({ } else if (type === "delete") {
current_page: page, await dataGenerationApi.deleteTask({ task_id: record.id });
page_size: pageSize, }
}); setSearchKeyword('');
setDatasetList(data.list); setPage(1);
getTaskList(1, pageSize, '');
message.success("操作成功");
} catch (error) {
console.error(error);
}
}; };
const getTaskList = async (page: number, pageSize: number, keyword?: string) => { const getDatasetList = async () => {
const { data } = await datasetApi.getDatasetList({
current_page: 1,
page_size: 100,
});
setDatasetList(data.items);
};
const getTaskList = async (
page: number,
pageSize: number,
keyword?: string
) => {
const { data } = await dataGenerationApi.getTaskList({ const { data } = await dataGenerationApi.getTaskList({
current_page: page, current_page: page,
page_size: pageSize, page_size: pageSize,
search_keyword: keyword search_keyword: keyword,
}); });
setDataSource(data.items); setDataSource(data.items);
setTotal(data.total); setTotal(data.total);
}; };
const getOptions = async () => {
const { data } = await dataGenerationApi.getOptions();
setTaskTypeOptions(
data.options.robotwin.task_types.map((item: any) => ({
value: item,
label: item,
}))
);
};
const clearPolling = () => { const clearPolling = () => {
PollyRef.current?.cancel(); PollyRef.current?.cancel();
PollyRef.current = null; PollyRef.current = null;
}; };
const startPolling = () => { const startPolling = (page: number, pageSize: number, keyword?: string) => {
PollyRef.current = preciseInterval(() => { PollyRef.current = preciseInterval(() => {
getTaskList(page, pageSize); getTaskList(page, pageSize, keyword);
}, refreshTime); }, refreshTime);
}; };
useEffect(() => { useEffect(() => {
clearPolling(); clearPolling();
getDatasetList(1, 10000) getDatasetList();
startPolling(); getOptions();
}, []); }, []);
const debounceStartPolling = debounce(
(page: number, pageSize: number, keyword?: string) => {
clearPolling();
startPolling(page, pageSize, keyword);
},
500
);
useEffect(() => {
debounceStartPolling(page, pageSize, searchKeyword);
}, [page, pageSize, searchKeyword]);
return ( return (
<> <>
<ColLayout HeaderSlot={HeaderSlot} FooterSlot={FooterSlot}> <ColLayout HeaderSlot={HeaderSlot} FooterSlot={FooterSlot}>
@ -155,7 +209,10 @@ const DataGenerationPage: React.FC<DataGenerationPageProps> = () => {
<Flex justify="space-between" align="center" className="operate"> <Flex justify="space-between" align="center" className="operate">
<Search <Search
placeholder="请输入任务名称模糊搜索" placeholder="请输入任务名称模糊搜索"
onSearch={(value) => getTaskList(page, pageSize, value)} onSearch={(value) => {
setSearchKeyword(value);
getTaskList(page, pageSize, value);
}}
style={{ width: "300px" }} style={{ width: "300px" }}
/> />
<ButtonGroup> <ButtonGroup>
@ -176,6 +233,8 @@ const DataGenerationPage: React.FC<DataGenerationPageProps> = () => {
onCancel={() => setCreateTaskModalVisible(false)} onCancel={() => setCreateTaskModalVisible(false)}
onConfirm={() => setCreateTaskModalVisible(false)} onConfirm={() => setCreateTaskModalVisible(false)}
datasetList={datasetList} datasetList={datasetList}
taskTypeOptions={taskTypeOptions}
onDatasetCreate={() => getDatasetList()}
></CreateTaskModal> ></CreateTaskModal>
)} )}
</> </>

View File

@ -5,6 +5,7 @@ import useToken from "antd/es/theme/useToken";
import React, { useEffect, useRef, useState } from "react"; import React, { useEffect, useRef, useState } from "react";
import DatasetForm, { DatasetFormRef } from "./DatasetForm"; import DatasetForm, { DatasetFormRef } from "./DatasetForm";
import { Dataset } from "@/types/dataset"; import { Dataset } from "@/types/dataset";
import dayjs from "dayjs";
interface DatasetCardProps { interface DatasetCardProps {
DatasetDetail: Dataset; DatasetDetail: Dataset;
@ -27,9 +28,12 @@ const DatasetCard: React.FC<DatasetCardProps> = ({
className="dataset_card_header" className="dataset_card_header"
justify="space-between" justify="space-between"
align="center" align="center"
wrap={false}
> >
<span>{DatasetDetail.name}</span> <Tooltip title={DatasetDetail.name}>
<span>{DatasetDetail.data_count} / </span> <span className="ellipsis" style={{flex: 1}}>{DatasetDetail.name}</span>
</Tooltip>
<span style={{ maxWidth: '180px' }}>{`${DatasetDetail.data_count} / 条 数据`}</span>
</Flex> </Flex>
<div className="dataset_card_content"> <div className="dataset_card_content">
<div className="dataset_card_content_description"> <div className="dataset_card_content_description">
@ -45,7 +49,7 @@ const DatasetCard: React.FC<DatasetCardProps> = ({
</div> </div>
<div className="dataset_card_content_description"> <div className="dataset_card_content_description">
<p className="description"></p> <p className="description"></p>
<p className="ellipsis">{DatasetDetail.created_at}</p> <p className="ellipsis">{dayjs(DatasetDetail.created_at).format('YYYY-MM-DD HH:mm:ss')}</p>
</div> </div>
</div> </div>
<Flex <Flex
@ -114,6 +118,7 @@ const useStyles = createStyles(({ token }) => {
color: token.colorText, color: token.colorText,
width: "100%", width: "100%",
marginBottom: "16px", marginBottom: "16px",
overflow: "hidden",
}, },
"& .dataset_card_content": { "& .dataset_card_content": {
"& .dataset_card_content_description": { "& .dataset_card_content_description": {

View File

@ -1,7 +1,7 @@
import ColLayout from "@/components/ColLayout"; import ColLayout from "@/components/ColLayout";
import React, { useEffect, useRef, useState } from "react"; import React, { Fragment, useEffect, useRef, useState } from "react";
import DatasetCard from "./components/DatasetCard"; import DatasetCard from "./components/DatasetCard";
import { Form, Input, message, Pagination, Spin } from "antd"; import { Empty, Flex, Form, Input, message, Pagination, Spin } from "antd";
import { datasetApi } from "@/api/dataset"; import { datasetApi } from "@/api/dataset";
import { preciseInterval } from "@/utils"; import { preciseInterval } from "@/utils";
import useToken from "antd/es/theme/useToken"; import useToken from "antd/es/theme/useToken";
@ -22,21 +22,7 @@ const DatasetPage: React.FC<DatasetPageProps> = (props: DatasetPageProps) => {
const refreshTime = 5000; const refreshTime = 5000;
const [searchKeyword, setSearchKeyword] = useState(""); const [searchKeyword, setSearchKeyword] = useState("");
const [operateFormVisible, setOperateFormVisible] = useState(false); const [operateFormVisible, setOperateFormVisible] = useState(false);
const [datasetList, setDatasetList] = useState<Dataset[]>([ const [datasetList, setDatasetList] = useState<Dataset[]>([]);
{
id: 1,
name: "数据集1",
description: "数据集1描述",
task_count: 1,
simulator: "simulator1",
task: "task1",
camera: "camera1",
storage_time: "2024-01-01",
created_at: "2024-01-01",
updated_at: "2024-01-01",
data_count: 1,
},
]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const HeaderSlot = ( const HeaderSlot = (
@ -65,7 +51,7 @@ const DatasetPage: React.FC<DatasetPageProps> = (props: DatasetPageProps) => {
getDatasetList(newPage, newPageSize); getDatasetList(newPage, newPageSize);
PollyRef.current = preciseInterval(() => { PollyRef.current = preciseInterval(() => {
getDatasetList(newPage, newPageSize); getDatasetList(newPage, newPageSize);
}, refreshTime); }, 5000);
}} }}
showSizeChanger showSizeChanger
></Pagination> ></Pagination>
@ -79,28 +65,14 @@ const DatasetPage: React.FC<DatasetPageProps> = (props: DatasetPageProps) => {
) => { ) => {
try { try {
setLoading(true); setLoading(true);
// const { data } = await datasetApi.getDatasetList({ const { data } = await datasetApi.getDatasetList({
// current_page: page, current_page: page,
// page_size: pageSize, page_size: pageSize,
// search_keyword: name, search_keyword: name,
// }); });
// setTotal(data.total); setTotal(data.total);
setDatasetList([
{
id: 1,
name: "数据集1",
description: "数据集1描述",
task_count: 1,
simulator: "simulator1",
task: "task1",
camera: "camera1",
storage_time: "2024-01-01",
created_at: "2024-01-01",
updated_at: "2024-01-01",
data_count: 1,
},
]);
setLoading(false); setLoading(false);
setDatasetList(data?.items || []);
} catch (error) { } catch (error) {
message.error("获取数据集列表失败"); message.error("获取数据集列表失败");
setLoading(false); setLoading(false);
@ -161,7 +133,7 @@ const DatasetPage: React.FC<DatasetPageProps> = (props: DatasetPageProps) => {
getDatasetList(page, pageSize); getDatasetList(page, pageSize);
PollyRef.current = preciseInterval(() => { PollyRef.current = preciseInterval(() => {
getDatasetList(page, pageSize); getDatasetList(page, pageSize);
}, refreshTime); }, 5000);
return () => { return () => {
clearPolling(); clearPolling();
}; };
@ -169,28 +141,36 @@ const DatasetPage: React.FC<DatasetPageProps> = (props: DatasetPageProps) => {
return ( return (
<ColLayout HeaderSlot={HeaderSlot} FooterSlot={FooterSlot}> <ColLayout HeaderSlot={HeaderSlot} FooterSlot={FooterSlot}>
{loading ? ( <div
<Spin /> className={styles.container}
) : ( onClick={() => setOperateFormVisible(false)}
<div >
className={styles.container} <Search
onClick={() => setOperateFormVisible(false)} className="search"
> placeholder="请输入数据集名称"
<Search className="search" placeholder="请输入数据集名称" onChange={(e) => { onChange={(e) => {
setSearchKeyword(e.target.value); setSearchKeyword(e.target.value);
getDatasetList(page, pageSize, e.target.value) getDatasetList(page, pageSize, e.target.value);
}} /> }}
{datasetList.map((item) => ( />
<DatasetCard {datasetList.length === 0 ? (
key={item.id} <Empty />
DatasetDetail={item} ) : (
onOperate={(type, dataset) => <Flex wrap gap={16}>
handleOperate(type, dataset as Dataset) {datasetList.map((item) => (
} <Fragment key={item.id}>
></DatasetCard> <DatasetCard
))} key={item.id}
</div> DatasetDetail={item}
)} onOperate={(type, dataset) =>
handleOperate(type, dataset as Dataset)
}
></DatasetCard>
</Fragment>
))}
</Flex>
)}
</div>
</ColLayout> </ColLayout>
); );
}; };
@ -200,10 +180,10 @@ export default DatasetPage;
const useStyles = createStyles(({ token }) => { const useStyles = createStyles(({ token }) => {
return { return {
container: { container: {
"& .search": { "& .search": {
width: "300px", width: "300px",
marginBottom: token.margin, marginBottom: token.margin,
} },
}, },
}; };
}); });

View File

@ -10,4 +10,13 @@ export default defineConfig({
"@": join(__dirname, "./src"), "@": join(__dirname, "./src"),
}, },
}, },
server: {
proxy: {
"/api": {
target: "http://120.48.128.161:30030",
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ""),
},
},
},
}) })