feat: initialize d-embodied project with basic layout and routing setup

This commit is contained in:
yue02.sun 2025-08-01 17:31:53 +08:00
commit 6746269b57
59 changed files with 7187 additions and 0 deletions

24
.gitignore vendored Normal file
View File

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

3
README.md Normal file
View File

@ -0,0 +1,3 @@
### api参数说明
1. 所有的分页查询列表请求request中固定包含current_page和page_size参数
2. 所有的请求,resposne body的格式都为{ status: number, message: string, data: any }

View File

@ -0,0 +1,31 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: d-embodied
namespace: bj1-dcloud
labels:
app.kubernetes.io/name: d-embodied
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: d-embodied
template:
metadata:
labels:
app.kubernetes.io/name: d-embodied
spec:
volumes:
- name: log-dir
emptyDir: {}
containers:
- name: d-embodied
image: >-
ccr-29eug8s3-vpc.cnc.bj.baidubce.com/dcloud/d-embodied:latest
ports:
- containerPort: 80
volumeMounts:
- name: log-dir
mountPath: /data/log
imagePullSecrets:
- name: ccr

View File

@ -0,0 +1,16 @@
apiVersion: v1
kind: Service
metadata:
name: d-embodied-svc
namespace: bj1-dcloud
labels:
app.kubernetes.io/name: d-embodied-svc
spec:
ports:
- protocol: TCP
port: 80
targetPort: 80
selector:
app.kubernetes.io/name: d-embodied
type: ClusterIP

View File

@ -0,0 +1,31 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: d-embodied
namespace: bj2-dcloud
labels:
app.kubernetes.io/name: d-embodied
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: d-embodied
template:
metadata:
labels:
app.kubernetes.io/name: d-embodied
spec:
volumes:
- name: log-dir
emptyDir: {}
containers:
- name: d-embodied
image: >-
ccr-29eug8s3-vpc.cnc.bj.baidubce.com/dcloud/d-embodied:latest
ports:
- containerPort: 80
volumeMounts:
- name: log-dir
mountPath: /data/log
imagePullSecrets:
- name: ccr

View File

@ -0,0 +1,16 @@
apiVersion: v1
kind: Service
metadata:
name: d-embodied-svc
namespace: bj2-dcloud
labels:
app.kubernetes.io/name: d-embodied-svc
spec:
ports:
- protocol: TCP
port: 80
targetPort: 80
selector:
app.kubernetes.io/name: d-embodied
type: ClusterIP

3
eslint.config.js Normal file
View File

@ -0,0 +1,3 @@
import antfu from "@antfu/eslint-config";
export default antfu({ react: true });

13
index.html Normal file
View File

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

4203
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

38
package.json Normal file
View File

@ -0,0 +1,38 @@
{
"name": "d-embodied",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite --host",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"antd": "^5.26.5",
"antd-style": "^3.7.1",
"axios": "^1.11.0",
"i18next": "^25.3.2",
"i18next-browser-languagedetector": "^8.2.0",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-i18next": "^15.6.0",
"react-router-dom": "^7.6.3",
"zustand": "^5.0.6"
},
"devDependencies": {
"@eslint/js": "^9.30.1",
"@types/node": "^24.0.14",
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",
"@vitejs/plugin-react": "^4.6.0",
"eslint": "^9.30.1",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20",
"globals": "^16.3.0",
"typescript": "~5.8.3",
"typescript-eslint": "^8.35.1",
"vite": "^7.0.4"
}
}

0
src/App.css Normal file
View File

93
src/App.tsx Executable file
View File

@ -0,0 +1,93 @@
/*
* @Author: yue02.sun yue02.sun@d-robotics.cc
* @Date: 2025-02-21 15:45:48
* @LastEditors: yue02.sun yue02.sun@d-robotics.cc
* @LastEditTime: 2025-03-28 16:58:20
* @FilePath: /trainer/projects/d-cms/src/App.tsx
* @Description: ,`customMade`, koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
*/
// 配置路由
import { RouterProvider, createBrowserRouter } from "react-router-dom";
import routes from "./router";
// 配置antd
import { App as AntApp, Button, Flex } from "antd";
import { ThemeProvider } from "antd-style";
import { getTheme, getCustomToken } from "./assets/styles/theme";
// 覆盖默认样式
import "@/assets/styles/reset.css";
// 引用通用样式
import "@/assets/styles/common.css";
import { useEffect, useState } from "react";
// 导入国际化配置
import "./i18n";
// import { user } from "@/apis/authorityApi/userApi";
import { FrownOutlined } from "@ant-design/icons";
// import { setRedirectUrl } from "@trainer/utils";
// import { systemStore } from "@/store/system.ts";
type UserAudit = "waiting" | "error" | "success";
function App() {
// const setUserInfo = systemStore((state) => state.setUserInfo);
// const [userAudit, setUserAudit] = useState<UserAudit>("waiting");
// const init = async () => {
// try {
// // 初始化用户信息
// const userInfoRep = await user.getUserInfo();
// if (userInfoRep.status !== 0 || !userInfoRep.data) {
// // data为null说明用户不存在
// setUserAudit("error");
// return;
// }
// setUserInfo(userInfoRep.data);
// setUserAudit("success");
// } catch (err) {
// setUserAudit("error");
// }
// };
// useEffect(() => {
// init();
// }, []);
return (
<ThemeProvider
appearance={"dark"}
theme={getTheme("dark")}
customToken={getCustomToken("dark")}
>
{/* {userAudit === "success" && (
<AntApp>
<RouterProvider
router={createBrowserRouter(routes, { basename: "/d-cms" })}
/>
</AntApp>
)} */}
{/* {userAudit === "error" && (
<Flex
style={{ height: "100%", fontSize: 20 }}
align="center"
justify="center"
>
<FrownOutlined style={{ marginRight: 8, fontSize: 30 }} />
<Button
color="primary"
variant="link"
style={{ fontSize: 16 }}
onClick={() => {
localStorage.removeItem("token");
setRedirectUrl();
}}
>
</Button>
</Flex>
)} */}
<AntApp>
<RouterProvider
router={createBrowserRouter(routes)}
/>
</AntApp>
</ThemeProvider>
);
}
export default App;

1
src/api/common.ts Normal file
View File

@ -0,0 +1 @@
export const ServerUrl = "/embolabApi/v1"

View File

@ -0,0 +1,40 @@
import { CameraType } from "@/types/data-generation";
import { ServerUrl } from "../common";
import request from "../request";
const baseUrl = `${ServerUrl}/data-generation-tasks`;
export const dataGenerationApi = {
getTaskList: (params: {
current_page: number;
page_size: number;
search_keyword?: string;
}) => {
return request.post(`${baseUrl}/list`, params);
},
createTask: (params: {
task_name: string;
simulator: string;
task_type: string;
camera: CameraType;
trajectory_count: number;
dataset_id: number;
}) => {
return request.post(`${baseUrl}`, params);
},
stopTask: (params: {
task_id: number;
}) => {
return request.post(`${baseUrl}/${params.task_id}/stop`);
},
deleteTask: (params: {
task_id: number;
}) => {
return request.delete(`${baseUrl}/${params.task_id}`);
},
getLogs: (params: {
task_id: number;
}) => {
return request.get(`${baseUrl}/${params.task_id}/logs`);
},
};

32
src/api/dataset/index.ts Normal file
View File

@ -0,0 +1,32 @@
import request from "../request";
import { ServerUrl } from "../common";
const baseUrl = `${ServerUrl}/datasets`;
export const datasetApi = {
getDatasetList: (params: {
current_page: number;
page_size: number;
search_keyword?: string;
}) => {
return request.post(`${baseUrl}/list`, params);
},
createDataset: (params: {
name: string;
description: string;
}) => {
return request.post(`${baseUrl}`, params);
},
deleteDataset: (params: {
id: number;
}) => {
return request.delete(`${baseUrl}/${params.id}`);
},
updateDataset: (params: {
id: number;
name: string;
description: string;
}) => {
return request.put(`${baseUrl}/${params.id}`, params);
},
}

210
src/api/request.ts Executable file
View File

@ -0,0 +1,210 @@
import { redirectTo } from "@/utils";
import axios, { AxiosRequestConfig } from "axios";
import { nanoid } from "nanoid";
import { systemStore } from "@/store/system";
import i18n from "../i18n";
// 存储请求的Mapkey为请求的唯一标识method + url
const pendingRequests = new Map<string, number>();
// 节流时间(毫秒)
const THROTTLE_TIME = 0;
/**
*
*/
const generateReqKey = (config: AxiosRequestConfig): string => {
const { method, url } = config;
return [method, url].join("&");
};
/**
*
*/
const addPendingRequest = (config: CustomAxiosRequestConfig) => {
const requestKey = generateReqKey(config);
const now = Date.now();
const lastRequestTime = pendingRequests.get(requestKey) || 0;
const throttleTime = config.throttleTime || THROTTLE_TIME;
// 如果在节流时间内有相同的请求则返回一个被reject的promise
if (now - lastRequestTime < throttleTime) {
return {
message: "Requests are too frequent",
};
}
// 更新最后请求时间
pendingRequests.set(requestKey, now);
// 设置一个定时器,在节流时间后清除这个请求记录
// 这样即使请求失败或响应拦截器没有清理,也能保证最终会被清理
const timer = setTimeout(() => {
pendingRequests.delete(requestKey);
}, throttleTime);
// 在请求配置中保存清理函数,以便在响应拦截器中调用
// @ts-ignore
config.clearThrottle = () => {
clearTimeout(timer);
pendingRequests.delete(requestKey);
};
return null;
};
// 创建一个用于导航的 hook
import type { InternalAxiosRequestConfig } from "axios";
interface CustomAxiosRequestConfig extends InternalAxiosRequestConfig {
throttleTime?: number;
}
const request = axios.create({
baseURL: "/api", // 替换为API基本URL
timeout: 3 * 10 * 1000, // 请求超时时间(毫秒)
});
// 请求拦截器
request.interceptors.request.use(
(config: CustomAxiosRequestConfig) => {
// 保证 headers 存在,避免 undefined 报错
config.headers = config.headers || {};
// 检查是否需要节流可通过config.throttleTime控制
if (config.throttleTime !== 0) {
const throttleError = addPendingRequest(config);
if (throttleError) {
return Promise.reject(throttleError);
}
}
const token = localStorage.getItem("token");
config.headers["Authorization"] = token;
config.headers["x-request-id"] = nanoid();
config.headers["SourceApp"] = "d_cloud";
// 从systemStore中获取当前语言设置并添加到请求头
const lang = systemStore.getState().lang;
config.headers["Language"] = lang.toLowerCase();
// if (import.meta.env.MODE === "development") {
// config.headers["d-user-id"] = 1;
// }
return config;
},
(error) => {
// 处理请求错误
return Promise.reject(error);
}
);
// 响应拦截器
request.interceptors.response.use(
(response) => {
// 请求成功,清理节流记录
// @ts-ignore
if (response.config.clearThrottle) {
// @ts-ignore
response.config.clearThrottle();
}
if (response.headers["sso-url"])
localStorage.setItem("ssoUrl", response.headers["sso-url"]);
// 在这里对响应数据进行处理
if (response.data.status === 0) {
return response.data;
}
if (response.data.status === 10002) {
redirectTo(
`${response.data.location}?redirectUrl=${window.location.href}`
);
return;
}
if (response.data.message) {
let urlPath = response.config.url || "";
if (urlPath.startsWith("/")) {
urlPath = urlPath.substring(1);
}
const moduleName = urlPath.split("/")?.[0] || "";
const translatedMessage = i18n.t(
`${moduleName}.${response.data.message}`,
{
defaultValue: response.data.message,
ns: "errormessage",
}
);
response.data.message = translatedMessage;
}
return Promise.reject(response.data);
},
(error) => {
// 请求失败,清理节流记录
if (error.config?.clearThrottle) {
error.config.clearThrottle();
}
// 如果是节流错误,直接返回
if (error?.message?.includes("请求过于频繁")) {
return Promise.reject(error);
}
// 处理响应错误
if (error.response) {
// const status = error.response.status;
// const data = error.response.data;
// switch (status) {
// case EErrorCode.REQUEST_ERROR:
// message.error("请求错误");
// break;
// case EErrorCode.TOKEN_EXPIRED:
// message
// .error("Token过期请重新登录")
// .then(() => {
// removeItem("token");
// removeItem("isLogin");
// removeItem("username");
// })
// .then(() => {
// window.location.href = "/login";
// });
// break;
// case EErrorCode.ACCESS_DENY:
// message.error("拒绝访问");
// break;
// case EErrorCode.NOT_FOUND:
// message.error(`请求地址出错: ${error.response.config.url}`);
// break;
// case EErrorCode.SERVER_INTER_ERROR:
// message.error("服务器内部错误");
// break;
// case EErrorCode.SERVICE_NOT_IMPLEMENT:
// message.error("服务未实现");
// break;
// case EErrorCode.GATEWAY_ERROR:
// message.error("网关错误");
// break;
// case EErrorCode.SERVICE_UNAVAILABLE:
// message.error("服务不可用");
// break;
// case EErrorCode.GATEWAY_TIMEOUT:
// message.error("网关超时");
// break;
// case EErrorCode.HTTP_NOT_SUPPORT:
// message.error("HTTP版本不受支持");
// break;
// default:
// // 处理其他HTTP错误
// console.error("HTTP Error", status, data);
// break;
// }
} else if (error.request) {
// 如果请求被发出,但没有收到响应
console.error("No response received", error.request);
} else {
// 发生了错误,请求无法发送
console.error("Error", error.message);
}
return Promise.reject(error);
}
);
export default request;

BIN
src/assets/images/logo_dark.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

BIN
src/assets/images/logo_light.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

85
src/assets/styles/common.css Executable file
View File

@ -0,0 +1,85 @@
.ellipsis {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.flex-width-auto {
flex: 0 0 auto;
}
.flex-1 {
flex: 1;
}
.flex-1-4-gap16 {
flex: 0 0 calc(25% - 12px);
margin: 0 16px 16px 0;
box-sizing: border-box;
}
.flex-1-4-gap16:nth-child(4n) {
margin-right: 0;
}
.full-wrapper {
height: 100%;
width: 100%;
overflow: hidden;
}
.full-height {
height: 100%;
}
.full-width {
width: 100%;
}
.overflow-hidden {
overflow: hidden;
}
.overflow-y-auto {
overflow-y: auto;
}
.tip-danger {
color: red;
font-size: small;
}
.ai-badge {
display: inline-block;
font-size: 10px;
color: #fff;
background-color: #f5222d;
padding: 2px 4px;
border-radius: 6px;
}
.ant-card .moderate-card {
border-radius: 12px;
box-shadow: 0px 4px 4px 0px #0000001A;
.ant-card-body {
padding: 16px;
}
}
.single-line-ellipsis {
white-space: nowrap; /* 禁止文本换行 */
overflow: hidden; /* 隐藏超出范围的内容 */
text-overflow: ellipsis; /* 使用省略号 */
}
.two-line-ellipsis {
word-break: break-all;
text-overflow: ellipsis;
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2; /* 这里是超出几行省略 */
}
.cursor-pointer {
cursor: pointer;
}
.oblique-text {
font-size: 12px;
font-style: oblique;
}
.tip-danger {
color: red;
font-size: small;
}
.rotated-icon-90 {
transform: rotate(90deg); /* 旋转 45 度 */
transition: transform 0.3s ease-in-out; /* 可选:平滑过渡效果 */
}

165
src/assets/styles/reset.css Executable file
View File

@ -0,0 +1,165 @@
html,
body,
div,
span,
applet,
object,
iframe,
h1,
h2,
h3,
h4,
h5,
h6,
p,
blockquote,
pre,
a,
abbr,
acronym,
address,
big,
cite,
code,
del,
dfn,
em,
img,
ins,
kbd,
q,
s,
samp,
small,
strike,
strong,
sub,
sup,
tt,
var,
b,
u,
i,
center,
dl,
dt,
dd,
ol,
ul,
li,
fieldset,
form,
label,
legend,
table,
caption,
tbody,
tfoot,
thead,
tr,
th,
td,
article,
aside,
canvas,
details,
embed,
figure,
figcaption,
footer,
header,
hgroup,
menu,
nav,
output,
ruby,
section,
summary,
time,
mark,
audio,
video {
padding: 0;
margin: 0;
/* font: inherit;
font-size: 14px;
vertical-align: baseline; */
border: 0;
}
/* HTML5 display-role reset for older browsers */
article,
aside,
details,
figcaption,
figure,
footer,
header,
hgroup,
menu,
nav,
section {
display: block;
}
body {
padding: 0;
margin: 0;
}
ol,
ul {
list-style: none;
}
blockquote,
q {
quotes: none;
}
blockquote::before,
blockquote::after,
q::before,
q::after {
content: "";
content: none;
}
table {
border-spacing: 0;
border-collapse: collapse;
}
html,
body,
#root {
width: 100%;
height: 100%;
}
::-webkit-scrollbar {
width: 6px; /* 纵向滚动条*/
height: 6px; /* 横向滚动条 */
background-color: transparent;
}
/*定义滚动条轨道 内阴影*/
::-webkit-scrollbar-track {
box-shadow: inset 0 0 6px rgba(0, 0, 0, 0);
background-color: transparent;
}
/*定义滑块 内阴影*/
::-webkit-scrollbar-thumb {
box-shadow: inset 0 0 6px rgba(0, 0, 0, 0);
background-color: rgba(125, 125, 125, 0.5);
border-radius: 8px;
}
micro-app {
height: 100%;
}
micro-app-body {
height: 100%;
}
.ant-app {
height: 100%;
}

180
src/assets/styles/theme.ts Normal file
View File

@ -0,0 +1,180 @@
import { theme } from "antd";
import { ThemeAppearance } from "antd-style";
const baseToken = {
colorInfo: "#fc7527",
colorWarning: "#ffe87e",
fontSizeSM: 12,
fontSizeLG: 16,
fontSizeXL: 20,
fontSizeHeading1: 32,
fontSizeHeading2: 30,
fontSizeHeading3: 20,
fontSizeHeading4: 18,
fontSizeHeading5: 16,
borderRadius: 8,
borderRadiusSM: 4,
borderRadiusXS: 2,
borderRadiusLG: 12,
fontSize: 14,
sizeStep: 4,
sizeUnit: 4,
marginXS: 8,
};
const baseForm = {
// labelColor: "#555961",
// labelFontSize: 12,
// labelHeight: 12,
verticalLabelPadding: 6,
// fontSize: 12,
// lineHeight: 1,
};
const baseButton = {
borderRadius: 8,
};
const baseModal = {
borderRadiusLG: 16,
borderRadius: 16,
};
const baseInput = {
// inputFontSize: 12,
lineHeight: 1,
colorTextPlaceholder: "#9DA2AE",
borderRadius: 8,
// fontSize: 12,
paddingBlock: 6.5,
};
const baseInputNumber = {
// inputFontSize: 12,
colorTextPlaceholder: "#9DA2AE",
borderRadius: 8,
lineHeight: 1,
paddingBlock: 7.5,
};
const baseTag = {
// fontSize: 12,
lineHeight: 1,
};
const baseRadio = {
// fontSize: 12,
lineHeight: 1,
radioSize: 12,
};
const baseSelect = {
// fontSize: 12,
lineHeight: 1,
colorTextPlaceholder: "#9DA2AE",
};
export const dark = {
theme: {
algorithm: theme.darkAlgorithm,
token: {
...baseToken,
colorPrimary: "#fd6b04",
colorBgBase: "#181818",
},
components: {
Button: {
...baseButton,
},
Form: {
...baseForm,
},
Modal: {
...baseModal,
},
Input: {
...baseInput,
},
InputNumber: {
...baseInputNumber,
},
Tag: {
...baseTag,
},
Radio: {
...baseRadio,
},
Select: {
...baseSelect,
},
Card: {},
},
},
customToken: {
layoutColor1: "#131319", //"#6e503c",
layoutColor2: "#131319",
mainBgColor: "#181818",
sideBgColor: "rgba(255, 255, 255, 0.02)",
activedMenuColor: "#555C64",
},
};
export const light = {
theme: {
algorithm: theme.defaultAlgorithm,
token: {
...baseToken,
colorBgBase: "#FcFcFc",
colorPrimary: "#ff5125",
colorBorder: "rgba(0, 0, 0, 0.06)",
},
components: {
Button: {
...baseButton,
defaultBorderColor: "rgba(0, 0, 0, 0.06)",
},
Form: {
...baseForm,
},
Table: {
borderColor: "transparent",
headerBg: "#fffcfb",
headerBorderRadius: 8,
},
Modal: {
...baseModal,
contentBg: "#f9f8f8",
footerBg: "#f9f8f8",
headerBg: "#f9f8f8",
},
Input: {
...baseInput,
},
InputNumber: {
...baseInputNumber,
},
Tag: {
...baseTag,
},
Radio: {
...baseRadio,
},
Select: {
...baseSelect,
},
},
},
customToken: {
layoutColor1: "#f8ebe6",
layoutColor2: "#f2f2f2",
sideBgColor: "rgba(255, 255, 255, 0.4)",
mainBgColor: "#F9F9F9",
activedMenuColor: "#F6F9FA",
},
};
export const getTheme = (appearance: ThemeAppearance) => {
switch (appearance) {
case "dark":
return dark.theme;
default:
return light.theme;
}
};
export const getCustomToken = (appearance: ThemeAppearance) => {
switch (appearance) {
case "dark":
return dark.customToken;
default:
return light.customToken;
}
};

View File

@ -0,0 +1,68 @@
import React, { ReactNode } from "react";
import { createStyles, useThemeMode } from "antd-style";
import { Card } from "antd";
type Props = {
HeaderSlot?: ReactNode;
children: ReactNode;
FooterSlot?: ReactNode;
style?: React.CSSProperties; // 新增style属性
};
const ColLayout: React.FC<Props> = (props: Props) => {
const { themeMode } = useThemeMode();
const { styles } = useStyles(themeMode);
const { children, HeaderSlot, FooterSlot, style } = props;
const Header = HeaderSlot ? (
<div className={styles.header}>{HeaderSlot}</div>
) : null;
const Footer = FooterSlot ? (
<div className={styles.footer}>{FooterSlot}</div>
) : null;
return (
<Card
className={styles.col_layout}
style={{ backgroundColor: "transparent" }}
>
{Header}
<div className={styles.main} style={style}>
{children}
</div>
{Footer}
</Card>
);
};
export default ColLayout;
const useStyles = createStyles(({ token, css }) => ({
col_layout: css`
height: 100%;
width: 100%;
/* background-color: #fff; */
box-sizing: border-box;
border: none;
> .ant-card-body {
padding: 0 10px;
height: 100%;
display: flex;
flex-direction: column;
background-color: transparent;
/* background-color: ${(token as any).mainBgColor}; */
}
`,
header: css`
height: auto;
padding: 20px 10px 10px;
`,
main: css`
flex: 1;
padding: 10px;
overflow-y: auto;
`,
footer: css`
height: auto;
padding: 10px 10px 20px;
`,
}));

26
src/components/Icon.tsx Executable file
View File

@ -0,0 +1,26 @@
/*
* @Author: yue02.sun yue02.sun@d-robotics.cc
* @Date: 2024-12-23 11:27:24
* @LastEditors: yue02.sun yue02.sun@d-robotics.cc
* @LastEditTime: 2025-05-23 16:24:59
* @LastEditTime: 2025-03-07 18:15:41
* @FilePath: /trainer/common/components/Icon/index.tsx
* @Description: ,`customMade`, koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
*/
import { createFromIconfontCN } from "@ant-design/icons";
import React, { HTMLAttributes } from "react";
const MyIcon = createFromIconfontCN({
scriptUrl: "//at.alicdn.com/t/c/font_4600944_zslwc585ws.js",
});
// 扩展HTMLAttributes以支持所有React事件和HTML属性
type Props = HTMLAttributes<HTMLSpanElement> & {
name: string;
};
const Icon: React.FC<Props> = ({ name, ...restProps }) => {
return <MyIcon type={`icon-${name}`} {...restProps} />;
};
export default Icon;

View File

@ -0,0 +1,125 @@
import { Button, Divider, Flex, Space } from "antd";
import { createStyles } from "antd-style";
import ButtonGroup from "antd/es/button/button-group";
import Search from "antd/es/input/Search";
import React, { useEffect, useRef, useState } from "react";
interface SearchBoxProps {
children: React.ReactNode;
placeholder?: string;
searchKey?: string | undefined;
onClear?: () => void;
onCancel?: () => void;
onConfirm?: () => void;
onSearch?: (searchKey: string) => void;
visible?: boolean;
}
const SearchBox: React.FC<SearchBoxProps> = ({
children,
placeholder = "请输入搜索内容",
searchKey,
onCancel,
onClear,
onConfirm,
onSearch,
visible,
}) => {
const { styles } = useStyles();
const [operateVisible, setOperateVisible] = useState(false);
const clickRef = useRef<HTMLDivElement>(null);
const handleClickOutside = (event: MouseEvent) => {
if (clickRef.current && !clickRef.current.contains(event.target as Node)) {
setOperateVisible(false);
}
};
useEffect(() => {
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, []);
return (
<div className={styles.searchBoxContainer}>
<Search
placeholder={placeholder}
className="search"
onClick={() => setOperateVisible(true)}
value={searchKey}
onChange={(e) => onSearch?.(e.target.value)}
/>
<Flex
vertical
ref={clickRef}
className="operate"
style={{ display: operateVisible ? "flex" : "none" }}
>
<div className="operate-content">{children}</div>
<Flex className="operate-area">
<Divider className="divider" />
<ButtonGroup className="button-group">
<Button onClick={() => {
onClear?.();
}}></Button>
<Space onClick={() => setOperateVisible(false)}>
<Button onClick={() => onCancel?.()}></Button>
<Button type="primary" onClick={() => onConfirm?.()}></Button>
</Space>
</ButtonGroup>
</Flex>
</Flex>
</div>
);
};
export default SearchBox;
const useStyles = createStyles(({ token }) => {
return {
searchBoxContainer: {
position: "relative",
width: "300px",
"& .search": {
width: "100%",
},
"& .operate": {
position: "absolute",
left: 0,
boxSizing: "border-box",
padding: `${token.padding}px ${token.padding}px 0`,
zIndex: 999,
backgroundColor: token.colorBgContainer,
boxShadow: token.boxShadow,
borderRadius: token.borderRadius,
width: "100%",
marginTop: token.marginXXS,
height: "300px",
overflow: "hidden",
},
"& .operate-content": {
flex: 1,
padding: `${token.paddingContentVertical}px 0`,
},
"& .operate-area": {
height: "50px",
position: "relative",
width: "100%",
"& .divider": {
margin: 0,
},
},
"& .button-group": {
position: "absolute",
transform: "translateY(-50%)",
top: 'calc(50% + 2px)',
right: 0,
display: 'flex',
justifyContent: 'space-between',
width: '100%'
},
},
};
});

81
src/i18n/index.ts Executable file
View File

@ -0,0 +1,81 @@
import i18n from "i18next";
import { initReactI18next } from "react-i18next";
import LanguageDetector from "i18next-browser-languagedetector";
// import { resources as sharedResources } from "@trainer/components/i18n";
import { systemStore } from "@/store/system";
// 加载所有语言包
const modules = import.meta.glob("./locales/**/*.json", {
eager: true,
}) as Record<string, { default: never }>;
export const localeTransitions = Object.entries(modules).reduce(
(prev: any, current) => {
const [path, module] = current;
const lang = path.match(/\/locales\/([\w-]+)\//);
const filename = path.match(/\/([\w-_]+)\.json$/);
if (filename && lang) {
prev[lang[1]] = prev[lang[1]] || {};
prev[lang[1]][filename[1]] = module.default;
} else {
console.error(`无法解析文件名称 path:${path}`);
}
return prev;
},
{}
);
// 合并共享组件的翻译资源
// const mergedResources = Object.keys(localeTransitions).reduce(
// (merged: Record<string, any>, lang: string) => {
// merged[lang] = {
// ...localeTransitions[lang],
// ...(sharedResources[lang as keyof typeof sharedResources] || {}),
// };
// return merged;
// },
// {}
// );
function getInitLang() {
let lang = systemStore.getState().lang;
if (lang) return lang;
// 检查浏览器语言
const browserLang = (
navigator.language ||
navigator.languages?.[0] ||
"en-US"
).toLowerCase();
lang = browserLang.startsWith("zh") ? "zh-CN" : "en-US";
systemStore.setState({ lang });
return lang;
}
const defaultLang = getInitLang();
i18n
// 检测用户当前使用的语言
// 文档: https://github.com/i18next/i18next-browser-languageDetector
.use(LanguageDetector)
// 注入 react-i18next 实例
.use(initReactI18next)
// 初始化 i18next
// 配置参数的文档: https://www.i18next.com/overview/configuration-options
.init({
debug: true,
lng: defaultLang, // 优先systemStore没有则浏览器自动检测
fallbackLng: "en",
interpolation: {
escapeValue: false,
},
detection: {
// 禁止所有缓存
caches: [],
},
resources: localeTransitions,
});
export default i18n;
export type langType = "zh-CN" | "en-US";

0
src/i18n/locales/zh-CN Normal file
View File

68
src/index.css Normal file
View File

@ -0,0 +1,68 @@
:root {
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}

172
src/layout/SiderBar.tsx Executable file
View File

@ -0,0 +1,172 @@
import { FC } from "react";
import { Tooltip } from "antd";
import Icon from "@/components/Icon";
import { createStyles } from "antd-style";
import { useNavigate, useLocation } from "react-router-dom";
import { useTranslation } from "react-i18next";
import { menuList } from "./menuList";
import type { MenuListItem } from "./menuList";
import LogoDark from "@/assets/images/logo_dark.png";
import LogoLight from "@/assets/images/logo_light.png";
import { systemStore } from "@/store/system";
const iconSize = 20;
const SiderBar: FC = () => {
const theme = systemStore((state) => state.theme);
const platform = systemStore((state) => state.platform);
const { styles } = useStyles();
const { pathname } = useLocation();
const navigate = useNavigate();
const goSettings = () => {
navigate("/settings");
};
const goManual = () => {
navigate("/manual");
};
const SiderBarItem = (props: {
menuItem: MenuListItem;
activedPath: string;
}) => {
const { styles } = useStyles();
const { icon, iconActive, path, name, disabled, activePath } =
props.menuItem;
const isActived = props.activedPath.includes(activePath);
const navigate = useNavigate();
const { t } = useTranslation("menu");
const goPage = () => {
if (disabled) return;
navigate(path);
};
return (
<Tooltip placement="right" title={props.menuItem.description}>
<div
className={`${styles.itemBox} ${
isActived ? styles.itemBoxActived : ""
} ${disabled ? styles.itemBoxDiasbled : ""}`}
onClick={goPage}
>
<div className="item-row">
<img className="item-icon" src={isActived ? iconActive : icon} />
<span style={{ textAlign: "justify" }}>{t(name)}</span>
</div>
</div>
</Tooltip>
);
};
const SiderBarList = menuList.map((menuItem) => (
<SiderBarItem
menuItem={menuItem}
activedPath={pathname}
key={menuItem.path}
/>
));
return (
<div className={styles.listBox}>
<div className="list-logo">
<img src={theme === "dark" ? LogoDark : LogoLight} alt="" />
{/* <span>D-Cloud</span> */}
</div>
<div className="list-main">{SiderBarList}</div>
<div className="list-bottom">
<div onClick={goSettings}>
<Icon name="config" style={{ fontSize: iconSize }}></Icon>
</div>
</div>
</div>
);
};
export default SiderBar;
const useStyles = createStyles(({ token, css }) => {
return {
itemBox: css`
margin: 5px 0;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
:hover {
background: ${token.colorPrimaryBgHover};
.item-row {
/* color: ${token.colorPrimary} !important; */
}
}
.item-row {
color: ${token.colorText};
/* color: #555961; */
font-size: 14px;
height: 44px;
line-height: 44px;
display: flex;
align-items: center;
.item-icon {
width: 35px;
height: 35px;
margin-right: 15px;
margin-left: 20px;
}
}
`,
itemBoxActived: css`
background: ${token.colorPrimaryBgHover};
position: relative;
.item-row {
color: ${token.colorPrimary} !important;
}
::after {
content: "";
display: block;
width: 4px;
height: 100%;
background: ${token.colorPrimary};
position: absolute;
right: 0;
top: 0;
}
`,
itemBoxDiasbled: css`
> .item-row {
color: ${token.colorTextDisabled} !important;
}
`,
listBox: css`
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
> .list-logo {
height: 50px;
margin-top: 10px;
/* padding-top: 24px; */
font-size: 22px;
/* margin-bottom: 30px; */
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
> img {
width: 42px;
height: 42px;
/* margin-right: 10px; */
}
}
> .list-main {
width: 100%;
flex: 1;
overflow-y: auto;
}
> .list-bottom {
height: 60px;
display: flex;
flex-direction: column;
justify-content: space-between;
cursor: pointer;
}
`,
};
});

66
src/layout/index.tsx Executable file
View File

@ -0,0 +1,66 @@
import { createStyles } from "antd-style";
import { Outlet } from "react-router-dom";
import SiderBar from "./SiderBar";
const Layout = () => {
const { styles } = useStyles();
return (
<div className={styles.layout_box}>
<div className={styles.layout_sider}>
<SiderBar></SiderBar>
</div>
<div className={styles.layout_main}>
<div className="layout-drag"></div>
<div className="layout-content">
<Outlet />
</div>
</div>
</div>
);
};
export default Layout;
const useStyles = createStyles(({ token, css }) => {
const padding = 0;
return {
layout_box: css`
height: 100vh;
display: flex;
flex-direction: row;
/* scrollbar-color: rebeccapurple green; */
background-image: linear-gradient(
${(token as any).layoutColor1},
${(token as any).layoutColor2},
${(token as any).layoutColor2}
);
`,
layout_sider: css`
width: 190px;
padding: ${padding}px 0;
background-color: ${(token as any).sideBgColor};
`,
layout_main: css`
position: relative;
flex: 1;
height: calc(100% - ${padding * 2}px);
overflow: hidden;
margin: ${padding}px;
margin-left: 0;
border-radius: ${token.borderRadius}px;
.layout-content {
height: 100%;
overflow-y: auto;
}
.layout-drag {
width: 100%;
height: 10px;
position: absolute;
top: -10px;
left: 0;
-webkit-app-region: drag;
}
`,
};
});

53
src/layout/menuList.ts Executable file
View File

@ -0,0 +1,53 @@
import DataTraining from "@/assets/images/navigate/datetrainning.png";
import Dataset from "@/assets/images/navigate/Dataset.png";
import DataTrainingActive from "@/assets/images/navigate/datetrainning-active.png";
import DatasetActive from "@/assets/images/navigate/dataset-active.png";
import Datagenerated from "@/assets/images/navigate/datagenerated.png";
import DatageneratedActive from "@/assets/images/navigate/datagenerated-active.png";
type IconType = "own" | "outer";
export interface MenuListItem {
iconType: IconType;
iconAddress: string;
path: string;
disabled?: boolean;
name: string;
description: string;
iconActive: string;
icon: string;
activePath: string;
}
export const menuList: MenuListItem[] = [
{
iconType: "own",
icon: Datagenerated,
iconAddress: "3yunpan",
path: "/data-create/list",
name: "数据生成",
description: "",
iconActive: DatageneratedActive,
activePath: "/data-create",
},
{
iconType: "own",
iconAddress: "jiankong",
iconActive: DatasetActive,
icon: Dataset,
path: "/data-set/list",
disabled: false,
name: "数据集",
description: "",
activePath: "/data-set",
},
{
iconType: "own",
iconAddress: "moxingxunlian1",
iconActive: DataTrainingActive,
icon: DataTraining,
path: "/model-workshop/list",
name: "模型工坊",
description: "",
activePath: "/model-workshop",
},
];

4
src/main.tsx Executable file
View File

@ -0,0 +1,4 @@
import ReactDOM from "react-dom/client";
import App from "./App.tsx";
ReactDOM.createRoot(document.getElementById("root")!).render(<App />);

75
src/router/index.tsx Normal file
View File

@ -0,0 +1,75 @@
import { Navigate } from "react-router-dom";
import { lazy, Suspense } from "react";
import { Spin } from "antd";
import type { RouteObject } from "react-router-dom";
import Layout from "@/layout";
// import { isPermissionPath } from "@trainer/utils";
// import { systemStore } from "@/store/system";
// const { setState, getState } = systemStore;
// 自定义懒加载函数
const lazyLoad = (factory: () => Promise<any>) => {
const Module = lazy(factory);
return (
<Suspense
fallback={
<Spin
size="large"
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
/>
}
>
<Module />
</Suspense>
);
};
// 路由 抽离成为JS对象形式
const routes: RouteObject[] = [
{
path: "/",
element: <Layout />,
// loader: loaderFn("*"),
children: [
{
index: true,
element: <Navigate to="/welcome" />,
},
{
path: "welcome",
element: lazyLoad(() => import("@/views/welcome")),
},
{
path: "data-create",
children: [
{
index: true,
element: <Navigate to="list" />,
},
{
path: "list",
element: lazyLoad(() => import("@/views/data-generation/index")),
}
]
},
{
path: "data-set",
children: [
{
index: true,
element: <Navigate to="list" />,
},
{
path: "list",
element: lazyLoad(() => import("@/views/dataset/index")),
}
]
},
],
},
];
export default routes;

59
src/store/system.ts Executable file
View File

@ -0,0 +1,59 @@
import { create, StateCreator } from "zustand";
import { persist } from "zustand/middleware";
export type ThemeType = "light" | "dark";
export type PlatformType = "macos" | "windows" | "linux" | "web";
interface IState {
platform: PlatformType;
setPlatform: (platform: PlatformType) => void;
theme: ThemeType;
setTheme: (theme: ThemeType) => void;
lang: string;
setLang: (lang: string) => void;
userInfo?: User.Info;
setUserInfo: (userInfo: User.Info) => void;
permission: any[];
setPermission: (permission: any[]) => void;
}
const systemSlice: StateCreator<IState, [["zustand/persist", unknown]]> = (
set
) => ({
platform: "web",
setPlatform: (platform: PlatformType) => {
return set({ platform: platform });
},
theme: "dark",
setTheme: (theme: ThemeType) => {
return set({ theme });
},
lang: "",
setLang: (lang: string) => {
return set({ lang });
},
userInfo: undefined,
setUserInfo: (userInfo: User.Info) => {
return set({ userInfo });
},
permission: [],
setPermission: (permission: any[]) => {
return set({ permission });
},
});
export const systemStore = create<IState>()(
persist(systemSlice, {
name: "system_store",
partialize: (state) => ({
theme: state.theme,
lang: state.lang,
}),
})
);

View File

@ -0,0 +1,108 @@
export enum TaskType {
adjust_bottle = "adjust_bottle",
beat_block_hammer = "beat_block_hammer",
blocks_ranking_rgb = "blocks_ranking_rgb",
blocks_ranking_size = "blocks_ranking_size",
click_alarmclock = "click_alarmclock",
click_bell = "click_bell",
dump_bin_bigbin = "dump_bin_bigbin",
grab_roller = "grab_roller",
handover_block = "handover_block",
handover_mic = "handover_mic",
hanging_mug = "hanging_mug",
lift_pot = "lift_pot",
move_can_pot = "move_can_pot",
move_playingcard_away = "move_playingcard_away",
move_stapler_pad = "move_stapler_pad",
open_laptop = "open_laptop",
open_microwave = "open_microwave",
pick_diverse_bottles = "pick_diverse_bottles",
pick_dual_bottles = "pick_dual_bottles",
place_a2b_left = "place_a2b_left",
place_a2b_right = "place_a2b_right",
place_bread_basket = "place_bread_basket",
place_bread_skillet = "place_bread_skillet",
place_burger_fries = "place_burger_fries",
place_can_basket = "place_can_basket",
place_cans_plasticbox = "place_cans_plasticbox",
place_container_plate = "place_container_plate",
place_dual_shoes = "place_dual_shoes",
place_empty_cup = "place_empty_cup",
place_fan = "place_fan",
place_mouse_pad = "place_mouse_pad",
place_object_basket = "place_object_basket",
place_object_scale = "place_object_scale",
place_object_stand = "place_object_stand",
place_phone_stand = "place_phone_stand",
place_shoe = "place_shoe",
press_stapler = "press_stapler",
put_bottles_dustbin = "put_bottles_dustbin",
put_object_cabinet = "put_object_cabinet",
rotate_qrcode = "rotate_qrcode",
scan_object = "scan_object",
shake_bottle_horizontally = "shake_bottle_horizontally",
shake_bottle = "shake_bottle",
stack_blocks_three = "stack_blocks_three",
stack_blocks_two = "stack_blocks_two",
stack_bowls_three = "stack_bowls_three",
stack_bowls_two = "stack_bowls_two",
stamp_seal = "stamp_seal",
turn_switch = "turn_switch",
move_pillbottle_pad = "move_pillbottle_pad",
}
export enum CameraType {
L515 = "L515",
D435 = "D435",
Large_L515 = "Large_L515",
Large_D435 = "Large_D435",
}

13
src/types/dataset.ts Normal file
View File

@ -0,0 +1,13 @@
export interface Dataset {
id: number;
name: string;
description: string;
task_count: number;
simulator: string;
task: string;
camera: string;
storage_time: string;
created_at: string;
updated_at: string;
data_count: number;
}

50
src/types/user.d.ts vendored Executable file
View File

@ -0,0 +1,50 @@
export enum UserState {
"活跃" = "activated", // 已激活
"未激活" = "unactivated", // 未激活
"已锁定" = "locked", // 已锁定
"已冻结" = "frozen", // 已冻结
"已注销" = "canceled", // 已注销
"已禁用" = "disabled", // 已禁用
"待审核" = "pending_review", // 待审核
"已删除" = "deleted", // 已删除
}
export enum UserIdentity {
"系统管理员" = "system_admin", // 系统管理员'
"个体用户" = "personal", // 独立个体
"组织用户" = "organization", // 组织
}
export enum OrganizationState {
= "unactivated", // 未激活
= "pending_review", // 待审核
= "activated", // 已激活
= "frozen", // 已冻结
= "deleted", // 已解散
}
declare global {
declare namespace User {
export interface OrganizationInfo {
id: number;
name: string;
role: string;
symbol: string; // 组织标识
state: OrganizationState;
createdAt: string;
}
export interface Info {
id: number;
userId:string;
userName: string;
mobile: string;
state: UserState;
// organizationName: string;
// organizationId: number;
source: string;
createdAt: string;
identity: UserIdentity;
role:string;
organization: OrganizationInfo;
}
}
}

1
src/types/vite-env.d.ts vendored Normal file
View File

@ -0,0 +1 @@
/// <reference types="vite/client" />

176
src/utils/index.ts Executable file
View File

@ -0,0 +1,176 @@
/*
* @Author: yue02.sun yue02.sun@d-robotics.cc
* @Date: 2024-12-02 19:17:14
* @LastEditors: yue02.sun yue02.sun@d-robotics.cc
* @LastEditTime: 2024-12-25 18:52:33
* @FilePath: /trainer/projects/d-cloud/src/utils/index.ts
* @Description: ,`customMade`, koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
*/
/**
*
*
* label和value
*
*
* @param enumObj
* @returns labelvalue
*/
export const enumToOptions = <T>(enumObj: { [key: string]: T }) => {
return Object.keys(enumObj).map((key) => {
return {
label: key,
value: enumObj[key],
};
});
};
/**
*
*
*
* undefined该函数通过Object.keys方法
* 使Array的find方法遍历键名数组
*
* @param enumObj
* @param value
* @returns undefined
*/
export const enumkeyByValue = <T>(enumObj: { [key: string]: T }, value: T) => {
return Object.keys(enumObj).find((key) => enumObj[key] === value);
};
export const preciseInterval = (callback: () => void, delay: number) => {
let start = performance.now();
let timer: NodeJS.Timeout | number;
function loop() {
const elapsed = performance.now() - start;
callback();
const nextInterval = delay - (elapsed % delay);
timer = setTimeout(loop, nextInterval);
}
timer = setTimeout(loop, delay);
return {
cancel: () => clearTimeout(timer),
};
};
export const calculateSha256 = async (file: File): Promise<string> => {
const sampleSize = 2 * 1024 * 1024; // 2MB per sample
const crypto = window.crypto.subtle;
// 创建进度提示
const key = "hashProgress";
// message.open({
// key,
// type: "loading",
// content: t("dataSpace:fileList.messages.fileSignatureCalculating"),
// duration: 0,
// });
try {
let combinedBuffer: ArrayBuffer;
if (file.size < 1024 * 1024 * 1024 * 2) {
// 如果文件小于4MB直接读取整个文件
const wholeFile = file.slice(0, file.size);
combinedBuffer = await new Promise<ArrayBuffer>((resolve, reject) => {
const reader = new FileReader();
reader.onload = (e) => resolve(e.target?.result as ArrayBuffer);
reader.onerror = reject;
reader.readAsArrayBuffer(wholeFile);
});
} else {
// 读取文件头部
const headChunk = file.slice(0, sampleSize);
const headBuffer = await new Promise<ArrayBuffer>((resolve, reject) => {
const reader = new FileReader();
reader.onload = (e) => resolve(e.target?.result as ArrayBuffer);
reader.onerror = reject;
reader.readAsArrayBuffer(headChunk);
});
// 读取文件尾部
const tailStart = file.size - sampleSize;
const tailChunk = file.slice(tailStart, file.size);
const tailBuffer = await new Promise<ArrayBuffer>((resolve, reject) => {
const reader = new FileReader();
reader.onload = (e) => resolve(e.target?.result as ArrayBuffer);
reader.onerror = reject;
reader.readAsArrayBuffer(tailChunk);
});
// 合并头尾数据
const combined = new Uint8Array(
headBuffer.byteLength + tailBuffer.byteLength + 8
);
combined.set(new Uint8Array(headBuffer), 0);
combined.set(new Uint8Array(tailBuffer), headBuffer.byteLength);
// 在末尾加入文件大小信息
const sizeBuffer = new Uint8Array(8);
const sizeView = new DataView(sizeBuffer.buffer);
sizeView.setBigUint64(0, BigInt(file.size), true);
combined.set(sizeBuffer, headBuffer.byteLength + tailBuffer.byteLength);
combinedBuffer = combined.buffer;
}
// 计算最终哈希
const hashBuffer = await crypto.digest("SHA-256", combinedBuffer);
const hashArray = Array.from(new Uint8Array(hashBuffer));
const hashHex = hashArray
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
// 完成后关闭进度提示
// message.open({
// key,
// type: "success",
// content: t("dataSpace:fileList.messages.fileSignatureCompleted"),
// duration: 2,
// });
return hashHex;
} catch (error) {
// 错误时关闭进度提示
// message.open({
// key,
// type: "error",
// content: t("dataSpace:fileList.messages.fileSignatureFailed"),
// duration: 2,
// });
console.error("Error calculating file signature:", error);
return "";
}
};
export const getPublicUrl = (path: string) => {
return `/d-cloud/${path}`;
};
// 将秒数转换为 HH:mm:ss 格式
export const formatTime = (seconds: number): string => {
if (seconds <= 0) return "00:00:00";
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const remainingSeconds = Math.floor(seconds % 60);
return (
[
hours.toString().padStart(2, "0"),
minutes.toString().padStart(2, "0"),
remainingSeconds.toString().padStart(2, "0"),
].join(":") + "s"
);
};
/**
* @description:
* @param {string} redirectUrl
* @return {*}
*/
export const redirectTo = (redirectUrl: string) => {
let timer = setTimeout(() => {
window.location.href = redirectUrl;
clearTimeout(timer);
}, 1000);
};

View File

@ -0,0 +1,159 @@
import {
Button,
Divider,
Flex,
Form,
Input,
InputNumber,
Modal,
Select,
Space,
Tooltip,
} from "antd";
import React, { useEffect } from "react";
interface CreateTaskModalProps {
visible: boolean;
onCancel: () => void;
onConfirm: (values: any) => void; // 修改onConfirm的类型以接收表单值
datasetList: { id: number; name: string; description: string }[] | undefined;
}
const CreateTaskModal: React.FC<CreateTaskModalProps> = ({
visible,
onCancel,
onConfirm,
datasetList,
}) => {
const [form] = Form.useForm();
const [datasetName, setDatasetName] = React.useState("");
const [loading, setLoading] = React.useState(false);
const handleOk = async () => {
try {
const values = await form.validateFields();
onConfirm(values);
form.resetFields(); // 提交成功后重置表单
} catch (errorInfo) {
console.log("Failed:", errorInfo);
}
};
const handleCreateDataset = async () => {
setLoading(true);
};
return (
<Modal
title="创建生成任务"
closable={false}
open={visible}
onCancel={onCancel}
onOk={handleOk} // 修改为handleOk
destroyOnHidden
>
<Form form={form} layout="vertical">
<Form.Item label="任务名称" name="task_name">
<Input placeholder="请输入任务名称(可选)" />
</Form.Item>
<Form.Item
label="仿真器"
name="simulator"
rules={[{ required: true, message: "请选择仿真器!" }]}
>
<Select placeholder="请选择仿真器">
<Select.Option value="robotwin">robotwin</Select.Option>
{/* 可以根据实际情况添加更多仿真器选项 */}
</Select>
</Form.Item>
<Form.Item
label="任务类型"
name="task_type"
rules={[{ required: true, message: "请选择任务类型!" }]}
>
<Select placeholder="请选择任务类型">
<Select.Option value="beat_block_hammer">
beat_block_hammer
</Select.Option>
{/* 可以根据实际情况添加更多任务类型选项 */}
</Select>
</Form.Item>
<Form.Item
label="相机类型"
name="camera"
rules={[{ required: true, message: "请选择相机类型!" }]}
>
<Select placeholder="请选择相机类型">
<Select.Option value="D435">D435</Select.Option>
{/* 可以根据实际情况添加更多相机类型选项 */}
</Select>
</Form.Item>
<Form.Item
label="轨迹条数"
name="trajectory_count"
rules={[{ required: true, message: "请输入轨迹条数!" }]}
>
<InputNumber
min={1}
max={10000}
style={{ width: "100%" }}
placeholder="请输入轨迹条数 (1-10000)"
/>
</Form.Item>
<Form.Item
label="数据集"
name="dataset_id"
rules={[{ required: true, message: "请录入数据集" }]}
>
<Select
placeholder="请选择数据集"
popupRender={(menu) => (
<div>
{menu}
<Divider style={{ margin: "8px 0" }} />
<Flex
style={{ width: "100%" }}
gap={16}
justify="space-between"
>
<Input
onChange={(e) => {
e.stopPropagation();
setDatasetName(e.target.value);
}}
value={datasetName}
placeholder="请输入数据集名称"
disabled={loading}
/>
<Button
type="primary"
onClick={(e) => {
e.stopPropagation();
handleCreateDataset();
}}
disabled={loading}
>
</Button>
</Flex>
</div>
)}
>
{datasetList?.map((item) => (
<Select.Option
key={item.id}
value={item.id}
title={item.description}
>
{item.name}
</Select.Option>
))}
</Select>
</Form.Item>
</Form>
</Modal>
);
};
export default CreateTaskModal;

View File

@ -0,0 +1,203 @@
import ColLayout from "@/components/ColLayout";
import { Button, Flex, Pagination, Table } from "antd";
import { createStyles } from "antd-style";
import ButtonGroup from "antd/es/button/button-group";
import Search from "antd/es/input/Search";
import React, { useEffect, useRef, useState } from "react";
import CreateTaskModal from "./components/CreateTaskModal";
import { datasetApi } from "@/api/dataset";
import useToken from "antd/es/theme/useToken";
import { dataGenerationApi } from "@/api/data-generation";
import { preciseInterval } from "@/utils";
// 假设的任务类型接口,如果已存在请忽略
interface TaskItem {
name: string;
status: string; // 假设 status 是字符串类型
type: string;
create_time: string;
update_time: string;
// 其他可能的字段
}
interface DataGenerationPageProps {}
const DataGenerationPage: React.FC<DataGenerationPageProps> = () => {
const { styles } = useStyles();
const [dataSource, setDataSource] = useState<TaskItem[] | undefined>(
undefined
);
const [createTaskModalVisible, setCreateTaskModalVisible] = useState(false);
const [datasetList, setDatasetList] = useState<
{ id: number; name: string; description: string }[] | undefined
>(undefined);
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
const [total, setTotal] = useState(0);
const [, token] = useToken();
const PollyRef = useRef<any>(null);
const refreshTime = 5000;
const HeaderSlot = (
<div>
<h1></h1>
</div>
);
const FooterSlot = (
<div className={styles.pagination}>
<Pagination
current={page}
pageSize={pageSize}
total={total}
onChange={(page) => {
setPage(page);
}}
onShowSizeChange={(_, pageSize) => {
setPageSize(pageSize);
}}
showSizeChanger
showQuickJumper
showTotal={(total) => `${total}`}
/>
</div>
);
const columns = [
{
title: "任务名称",
dataIndex: "name",
key: "name",
},
{
title: "任务状态",
dataIndex: "status",
key: "status",
},
{
title: "任务类型",
dataIndex: "type",
key: "type",
},
{
title: "任务创建时间",
dataIndex: "create_time",
key: "create_time",
},
{
title: "任务更新时间",
dataIndex: "update_time",
key: "update_time",
},
{
title: "操作",
key: "action",
render: (_: any, record: TaskItem) => (
<ButtonGroup>
{record.status === "running" ? ( // 假设 "running" 表示执行中
<>
<Button type="primary"></Button>
<Button type="primary"></Button>
</>
) : (
<>
<Button type="primary"></Button>
<Button type="primary"></Button>
</>
)}
</ButtonGroup>
),
},
];
const getDatasetList = async (
page: number,
pageSize: number,
) => {
const { data } = await datasetApi.getDatasetList({
current_page: page,
page_size: pageSize,
});
setDatasetList(data.list);
};
const getTaskList = async (page: number, pageSize: number, keyword?: string) => {
const { data } = await dataGenerationApi.getTaskList({
current_page: page,
page_size: pageSize,
search_keyword: keyword
});
setDataSource(data.items);
setTotal(data.total);
};
const clearPolling = () => {
PollyRef.current?.cancel();
PollyRef.current = null;
};
const startPolling = () => {
PollyRef.current = preciseInterval(() => {
getTaskList(page, pageSize);
}, refreshTime);
};
useEffect(() => {
clearPolling();
getDatasetList(1, 10000)
startPolling();
}, []);
return (
<>
<ColLayout HeaderSlot={HeaderSlot} FooterSlot={FooterSlot}>
<div className={styles.wrapper}>
<Flex justify="space-between" align="center" className="operate">
<Search
placeholder="请输入任务名称模糊搜索"
onSearch={(value) => getTaskList(page, pageSize, value)}
style={{ width: "300px" }}
/>
<ButtonGroup>
<Button
type="primary"
onClick={() => setCreateTaskModalVisible(true)}
>
</Button>
</ButtonGroup>
</Flex>
<Table columns={columns} dataSource={dataSource}></Table>
</div>
</ColLayout>
{createTaskModalVisible && (
<CreateTaskModal
visible={createTaskModalVisible}
onCancel={() => setCreateTaskModalVisible(false)}
onConfirm={() => setCreateTaskModalVisible(false)}
datasetList={datasetList}
></CreateTaskModal>
)}
</>
);
};
export default DataGenerationPage;
const useStyles = createStyles(({ token }) => {
return {
wrapper: {
height: "100%",
width: "100%",
overflow: "hidden",
"& .operate": {
marginBottom: token.margin,
},
},
pagination: {
position: "absolute",
bottom: token.margin,
right: token.margin,
},
};
});

View File

@ -0,0 +1,132 @@
import { Button, Flex, Modal, Popconfirm, Popover, Space, Tooltip } from "antd";
import { createStyles } from "antd-style";
import ButtonGroup from "antd/es/button/button-group";
import useToken from "antd/es/theme/useToken";
import React, { useEffect, useRef, useState } from "react";
import DatasetForm, { DatasetFormRef } from "./DatasetForm";
import { Dataset } from "@/types/dataset";
interface DatasetCardProps {
DatasetDetail: Dataset;
onOperate?: (type: 'edit' | 'delete', dataset: Dataset) => void;
}
const DatasetCard: React.FC<DatasetCardProps> = ({
DatasetDetail,
onOperate,
}) => {
const { styles } = useStyles();
const [, token] = useToken();
const formRef = useRef<DatasetFormRef>(null);
const [editModalVisible, setEditModalVisible] = useState(false);
return (
<>
<Flex className={styles.dataset_card} vertical>
<Flex
className="dataset_card_header"
justify="space-between"
align="center"
>
<span>{DatasetDetail.name}</span>
<span>{DatasetDetail.data_count} / </span>
</Flex>
<div className="dataset_card_content">
<div className="dataset_card_content_description">
<p className="description"></p>
<Tooltip title={DatasetDetail.description}>
<p
className="ellipsis"
style={{ height: "36px", lineHeight: "16px" }}
>
{DatasetDetail.description}
</p>
</Tooltip>
</div>
<div className="dataset_card_content_description">
<p className="description"></p>
<p className="ellipsis">{DatasetDetail.created_at}</p>
</div>
</div>
<Flex
className="dataset_card_footer"
align="center"
justify="space-between"
style={{ flex: 1 }}
>
<span className="cursor-pointer" style={{ color: token.colorPrimary }}></span>
<ButtonGroup>
<Space>
<Button color="cyan" variant="solid" onClick={() => setEditModalVisible(true)}> {/* 点击编辑按钮显示弹窗 */}
</Button>
<Popconfirm
title="确定删除吗?"
onConfirm={() => onOperate?.("delete", DatasetDetail)}
>
<Button color="danger" variant="solid">
</Button>
</Popconfirm>
</Space>
</ButtonGroup>
</Flex>
</Flex>
<Modal
title="编辑数据集"
open={editModalVisible} // 使用状态控制弹窗显示
onCancel={() => setEditModalVisible(false)} // 取消时关闭弹窗
onOk={async () => {
try {
const values = await formRef.current?.validateFields();
if (values) {
onOperate?.("edit", { ...DatasetDetail, ...values }); // 合并 DatasetDetail 和 values
setEditModalVisible(false);
}
} catch (error) {
console.error("表单验证失败:", error);
}
}}
>
<DatasetForm ref={formRef} initialValues={DatasetDetail} />
</Modal>
</>
);
};
export default DatasetCard;
const useStyles = createStyles(({ token }) => {
return {
dataset_card: {
width: "360px",
height: "240px",
borderRadius: "16px",
border: "1px solid rgba(255, 81, 37, 0.15)",
background: `linear-gradient(to left top, #ff5125, ${token.colorBgBase})`,
boxShadow: "0 8px 24px rgba(255, 81, 37, 0.08)",
boxSizing: "border-box",
padding: "24px 24px 16px",
lineHeight: "1",
"& .dataset_card_header": {
fontWeight: "600",
fontSize: "24px",
color: token.colorText,
width: "100%",
marginBottom: "16px",
},
"& .dataset_card_content": {
"& .dataset_card_content_description": {
color: token.colorTextBase,
padding: "8px 0",
fontSize: "14px",
"& .description": {
color: token.colorTextDescription,
marginBottom: "8px",
},
},
},
"& .dataset_card_footer": {},
},
};
});

View File

@ -0,0 +1,56 @@
import { Form, Input } from "antd";
import React, { useImperativeHandle, forwardRef } from "react";
interface DatasetFormProps {
initialValues?: {
name?: string;
description?: string;
};
}
export interface DatasetFormRef {
resetFields: () => void;
validateFields: () => Promise<{ name: string; description: string }>;
getFieldsValue: () => { name: string; description: string };
}
const DatasetForm: React.ForwardRefRenderFunction<DatasetFormRef, DatasetFormProps> = (
{ initialValues },
ref
) => {
const [form] = Form.useForm();
useImperativeHandle(ref, () => ({
resetFields: () => {
form.resetFields();
},
validateFields: () => {
return form.validateFields();
},
getFieldsValue: () => {
return form.getFieldsValue();
},
}));
return (
<Form form={form} layout="vertical" initialValues={initialValues}>
<Form.Item
label="数据集名称"
name="name"
rules={[{ required: true, message: "请输入数据集名称" }]}
>
<Input placeholder="请输入数据集名称" />
</Form.Item>
<Form.Item
label="数据集描述"
name="description"
rules={[{ required: true, message: "请输入数据集描述" }]}
>
<Input.TextArea rows={4} placeholder="请输入数据集描述" />
</Form.Item>
</Form>
);
};
export default forwardRef(DatasetForm);

209
src/views/dataset/index.tsx Normal file
View File

@ -0,0 +1,209 @@
import ColLayout from "@/components/ColLayout";
import React, { useEffect, useRef, useState } from "react";
import DatasetCard from "./components/DatasetCard";
import { Form, Input, message, Pagination, Spin } from "antd";
import { datasetApi } from "@/api/dataset";
import { preciseInterval } from "@/utils";
import useToken from "antd/es/theme/useToken";
import { Dataset } from "@/types/dataset";
import Search from "antd/es/input/Search";
import { createStyles } from "antd-style";
import SearchBox from "@/components/SearchBox";
interface DatasetPageProps {}
const DatasetPage: React.FC<DatasetPageProps> = (props: DatasetPageProps) => {
const { styles } = useStyles();
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
const [total, setTotal] = useState(0);
const PollyRef = useRef<any>(null);
const [, token] = useToken();
const refreshTime = 5000;
const [searchKeyword, setSearchKeyword] = useState("");
const [operateFormVisible, setOperateFormVisible] = useState(false);
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 HeaderSlot = (
<div>
<h1></h1>
</div>
);
const FooterSlot = (
<div
style={{
position: "absolute",
bottom: token.marginXXL,
zIndex: 1,
right: token.padding,
}}
>
<Pagination
current={page}
pageSize={pageSize}
total={total}
onChange={(newPage, newPageSize) => {
setPage(newPageSize);
setPageSize(newPageSize);
clearPolling();
getDatasetList(newPage, newPageSize);
PollyRef.current = preciseInterval(() => {
getDatasetList(newPage, newPageSize);
}, refreshTime);
}}
showSizeChanger
></Pagination>
</div>
);
const getDatasetList = async (
page: number,
pageSize: number,
name?: string
) => {
try {
setLoading(true);
// const { data } = await datasetApi.getDatasetList({
// current_page: page,
// page_size: pageSize,
// search_keyword: name,
// });
// 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);
} catch (error) {
message.error("获取数据集列表失败");
setLoading(false);
}
};
const deleteDataset = async (id: number) => {
try {
setLoading(true);
await datasetApi.deleteDataset({ id });
message.success("删除成功");
setLoading(false);
} catch (error) {
message.error("删除失败");
setLoading(false);
}
};
const editDataset = async ({
id,
name,
description,
}: {
id: number;
name: string;
description: string;
}) => {
try {
setLoading(true);
await datasetApi.updateDataset({ id, name, description });
message.success("编辑成功");
setLoading(false);
} catch (error) {
message.error("编辑失败");
setLoading(false);
}
};
const handleOperate = (type: "edit" | "delete", dataset: Dataset) => {
if (type === "edit") {
editDataset({
id: dataset.id,
name: dataset.name,
description: dataset.description,
});
} else {
deleteDataset(dataset.id);
}
};
const clearPolling = () => {
PollyRef.current?.cancel();
PollyRef.current = null;
};
useEffect(() => {
clearPolling();
getDatasetList(page, pageSize);
PollyRef.current = preciseInterval(() => {
getDatasetList(page, pageSize);
}, refreshTime);
return () => {
clearPolling();
};
}, []);
return (
<ColLayout HeaderSlot={HeaderSlot} FooterSlot={FooterSlot}>
{loading ? (
<Spin />
) : (
<div
className={styles.container}
onClick={() => setOperateFormVisible(false)}
>
<Search className="search" placeholder="请输入数据集名称" onChange={(e) => {
setSearchKeyword(e.target.value);
getDatasetList(page, pageSize, e.target.value)
}} />
{datasetList.map((item) => (
<DatasetCard
key={item.id}
DatasetDetail={item}
onOperate={(type, dataset) =>
handleOperate(type, dataset as Dataset)
}
></DatasetCard>
))}
</div>
)}
</ColLayout>
);
};
export default DatasetPage;
const useStyles = createStyles(({ token }) => {
return {
container: {
"& .search": {
width: "300px",
marginBottom: token.margin,
}
},
};
});

View File

@ -0,0 +1,36 @@
import React from "react";
import { Layout, Typography, Button } from "antd";
import Icon from "@/components/Icon";
import { createStyles } from "antd-style";
const WelcomePage: React.FC = () => {
const { styles } = useStyles();
return (
<div
style={{
height: "100%",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<div style={{ textAlign: "center" }}>
<Icon
name="huanyingmoshi"
style={{ fontSize: 40 }}
className={styles.welcon_icon}
/>
</div>
</div>
);
};
export default WelcomePage;
const useStyles = createStyles(({ token, css }) => {
return {
welcon_icon: css`
/* color: ${token.colorPrimary}; */
`,
};
});

28
tsconfig.app.json Normal file
View File

@ -0,0 +1,28 @@
{
"compilerOptions": {
"target": "ES2022",
"useDefineForClassFields": true,
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
// "paths": {
// "@demo": ["projects/demo/src"]
// }
}
}

12
tsconfig.json Normal file
View File

@ -0,0 +1,12 @@
{
"extends": "./tsconfig.app.json",
"files": [],
"references": [],
"include": ["src/**/*", "src/types/**/*", "vite.config.ts"],
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
}
}

10
tsconfig.node.json Normal file
View File

@ -0,0 +1,10 @@
{
"compilerOptions": {
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true,
"strict": true,
"noEmit": true
}
}

13
vite.config.ts Normal file
View File

@ -0,0 +1,13 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import fs from "fs";
import path, { join } from "path";
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
"@": join(__dirname, "./src"),
},
},
})