feat: initialize d-embodied project with basic layout and routing setup
24
.gitignore
vendored
Normal 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
@ -0,0 +1,3 @@
|
||||
### api参数说明
|
||||
1. 所有的分页查询列表请求,request中固定包含current_page和page_size参数
|
||||
2. 所有的请求,resposne body的格式都为{ status: number, message: string, data: any }
|
||||
31
deploy/k8s/bj1/deployment.yaml
Normal 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
|
||||
16
deploy/k8s/bj1/service.yaml
Normal 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
|
||||
|
||||
31
deploy/k8s/bj2/deployment.yaml
Normal 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
|
||||
16
deploy/k8s/bj2/service.yaml
Normal 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
@ -0,0 +1,3 @@
|
||||
import antfu from "@antfu/eslint-config";
|
||||
|
||||
export default antfu({ react: true });
|
||||
13
index.html
Normal 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
38
package.json
Normal 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
93
src/App.tsx
Executable 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
@ -0,0 +1 @@
|
||||
export const ServerUrl = "/embolabApi/v1"
|
||||
40
src/api/data-generation/index.ts
Normal 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
@ -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
@ -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";
|
||||
|
||||
// 存储请求的Map,key为请求的唯一标识(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
|
After Width: | Height: | Size: 5.2 KiB |
BIN
src/assets/images/logo_light.png
Executable file
|
After Width: | Height: | Size: 5.1 KiB |
BIN
src/assets/images/navigate/Dataset.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
src/assets/images/navigate/cloud-disk-active.png
Normal file
|
After Width: | Height: | Size: 5.5 KiB |
BIN
src/assets/images/navigate/cloud-disk-disabled.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
src/assets/images/navigate/cloud-disk.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
src/assets/images/navigate/datagenerated-active.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
src/assets/images/navigate/datagenerated.png
Normal file
|
After Width: | Height: | Size: 6.3 KiB |
BIN
src/assets/images/navigate/dataset-active.png
Normal file
|
After Width: | Height: | Size: 6.6 KiB |
BIN
src/assets/images/navigate/dataset-disabled.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
src/assets/images/navigate/datetrainning-active.png
Normal file
|
After Width: | Height: | Size: 5.0 KiB |
BIN
src/assets/images/navigate/datetrainning-disabled.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
src/assets/images/navigate/datetrainning.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
85
src/assets/styles/common.css
Executable 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
@ -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
@ -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;
|
||||
}
|
||||
};
|
||||
68
src/components/ColLayout/index.tsx
Executable 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
@ -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;
|
||||
125
src/components/SearchBox/index.tsx
Normal 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
@ -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
68
src/index.css
Normal 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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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,
|
||||
}),
|
||||
})
|
||||
);
|
||||
108
src/types/data-generation.ts
Normal 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
@ -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
@ -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
@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
176
src/utils/index.ts
Executable 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 返回一个选项数组,每个选项对象包含label(原枚举对象的键)和value(原枚举对象的值)
|
||||
*/
|
||||
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);
|
||||
};
|
||||
159
src/views/data-generation/components/CreateTaskModal.tsx
Normal 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;
|
||||
203
src/views/data-generation/index.tsx
Normal 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,
|
||||
},
|
||||
};
|
||||
});
|
||||
132
src/views/dataset/components/DatasetCard.tsx
Normal 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": {},
|
||||
},
|
||||
};
|
||||
});
|
||||
56
src/views/dataset/components/DatasetForm.tsx
Normal 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
@ -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,
|
||||
}
|
||||
},
|
||||
};
|
||||
});
|
||||
36
src/views/welcome/index.tsx
Normal 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
@ -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
@ -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
@ -0,0 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"noEmit": true
|
||||
}
|
||||
}
|
||||
13
vite.config.ts
Normal 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"),
|
||||
},
|
||||
},
|
||||
})
|
||||