&ThNv
z#G6$|HpzZH%0G*LqZpZM{$2aa`|$rtzfI$N8um2oX?$yu`!q9VD<4#<5M#n(ql(j1}&As?R5XG$@y;hViJ%AA_x-Fl=^
zEc7iGvi)%S@z~FN^)DCHwbS1$+x@MwgY3SwO7Z^IP;ru)e-Qds?4YChZhT|ZmDNN1
zj6P4{!p|&5`?}_wUo<7iow?W18j4qsQo4Cw{XVI{!$2gP46#i#sj?TexuhS-vQiU
zebn9E7<1#~AX}4V*_`k+--jPc^Y3Qk{Ug{r_+F;he9lrIwolD{eEZbGr{8?oKAx3Y
zu>YYvhrShC^Vdf`?Y}eT$vRRmcwIi%i(P&=W3cPjin(vWKMDJkmRmRlTMPCnCHSXc
zpJY8t?$5*iF!XKsW8Ut@xEo&|E#sdVoG|(Od5e2MIV-05cb%p1UuA!ZUCS56FW9oo
zxzTVPS@DwW<2>(qWvTH@(;j7}#yzQJPe^;jwPC;C<&Sy62K>!2FWwoi$Tt6Xi$3i7
zR~h89F?zioooTAxO89<{++QN|AJO|$^xG7@hKf^oonpqO=yNS{pQ64mz+FarCU?ya
zHpa{Nuf$dETN73M4{1I;3H2arje2XYVe<@gu;0vcM0B2o{bu%?A&RtU*Go~JwJtK={?y|uOFJTU|sjSVZ&4GU~{5!b8E8l$R_#Un5xR(
z&WHZhv$D|x>}@Y~ed{xgGgNU8u*0)_k6wERy@l`Fy3f@fW9Dq%H+45`&0m0jM)_}7
zN^w$faURy)A>2vc8ae&=2=eXL}uTysy_EWHZYOdLa?*-IY
z^;^W=+nT7{f?eL8bdPOMdb0g&S#Dey(|o_cvjqNO%Bo`@V?Tc!_D}Kr8+z?!=Ik;v
zewluorQhB{YJa)X9(KOmuDZuzUsT-AsQXX)?a$QwGIQ{IYWzFAUfP@}zex5^ZA^Nf
zTdNL9!yb`(a-z#`I60mL?XKn$YPRfN*nMiBH!Ak=%ya_Jzk;pKly~v{^UkpQJnZYH
z-tLvosP{1Ze4R4aa*9s^v%o=~U
z+0x`W0lt1-f8#vQOhe8&*gN6Aeh2nPCUf__^)a_h#z)D$+uy3z@MUwdA|L+bIl1IM
zA-4wJmSE=T2(8x@odG!vdmrPIzi@8L8fVsO#p!2F!M}?4cVNFu<}EbY87+OWJ>m#w
z;XV07-*`-FeD>Jc*dg{O{c`cigzWyHBAs#0ZO*Nh>l`KfhdU#sC*YqW@2_o4czvw$
zO}=dBha>ctnXOOy?oadiYq^}P!tG?pf3zrTWzJ5X9Xl0Q6jMDEm4p!6R!%hY86
literal 0
HcmV?d00001
diff --git a/frontend/providers/workorder/public/icons/download.svg b/frontend/providers/workorder/public/icons/download.svg
new file mode 100644
index 000000000..8aed8eea5
--- /dev/null
+++ b/frontend/providers/workorder/public/icons/download.svg
@@ -0,0 +1,3 @@
+
diff --git a/frontend/providers/workorder/public/icons/sealos.svg b/frontend/providers/workorder/public/icons/sealos.svg
new file mode 100644
index 000000000..70e732656
--- /dev/null
+++ b/frontend/providers/workorder/public/icons/sealos.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/providers/workorder/public/icons/token.svg b/frontend/providers/workorder/public/icons/token.svg
new file mode 100644
index 000000000..b0dd76f35
--- /dev/null
+++ b/frontend/providers/workorder/public/icons/token.svg
@@ -0,0 +1,10 @@
+
diff --git a/frontend/providers/workorder/public/icons/wechat.svg b/frontend/providers/workorder/public/icons/wechat.svg
new file mode 100644
index 000000000..b713640cb
--- /dev/null
+++ b/frontend/providers/workorder/public/icons/wechat.svg
@@ -0,0 +1,4 @@
+
diff --git a/frontend/providers/workorder/public/locales/en/common.json b/frontend/providers/workorder/public/locales/en/common.json
new file mode 100644
index 000000000..1811debd7
--- /dev/null
+++ b/frontend/providers/workorder/public/locales/en/common.json
@@ -0,0 +1,64 @@
+{
+ "All": "All",
+ "Are you sure you want to submit this work order": "Are you sure you want to submit this work order?",
+ "Cancel": "Cancel",
+ "Close": "Close",
+ "Completed": "Completed",
+ "Confirm": "Confirm",
+ "Create Time": "Create Time ",
+ "Creating": "Creating",
+ "Delete": "Delete",
+ "Deleted": "Deleted",
+ "Description": "Description",
+ "Details": "Details",
+ "Failed": "Failed",
+ "File upload failed": "File upload failed",
+ "functionserver": "Function Server",
+ "Name": "Name",
+ "New Order": "New Order",
+ "None": "None",
+ "Notification SwitchToManual Tips": "The request was sent successfully, please wait patiently.",
+ "objectstorage": "Object Storage",
+ "Operation": "Operation",
+ "Order Conversation": "Order Conversation",
+ "Order Detail": "Order Detail",
+ "Order Empty": "Order Empty",
+ "Order List": "Order List",
+ "Order Name": "Order Name",
+ "Page": "Page",
+ "Pending": "Pending",
+ "Problem Description": "Problem Description",
+ "Processing": "Processing",
+ "Prompt": "Prompt",
+ "Question Type": "Question Type",
+ "Request exceeds limit": "Request exceeds limit",
+ "Send": "Send",
+ "Status": "Status",
+ "Submit Error": "Submit Error",
+ "Submit Order": "Submit Order",
+ "Switch to manual": "Switch to manual",
+ "Total": "Total",
+ "Type": "Type",
+ "Upload File": "Upload File",
+ "appendix": "appendix",
+ "applaunchpad": "App Launchpad",
+ "cloudserver": "Cloud Server",
+ "costcenter": "CostCenter",
+ "cronjob": "CronJob",
+ "dbprovider": "Database",
+ "describe your problem": "describe your problem",
+ "file": {
+ "Upload Success": "File uploaded successfully",
+ "Uploading": "Uploading file, Progress: {{percent}}%"
+ },
+ "network anomaly": "Network anomaly",
+ "please wait for the end": "Chatting in progress... Please wait for it to finish",
+ "template": "App Store",
+ "user info": "User Info",
+ "send": "Send",
+ "newline": "newline",
+ "Copy": "Copy",
+ "api is error": "api is error",
+ "region": "region",
+ "userId": "userId"
+}
diff --git a/frontend/providers/workorder/public/locales/zh/common.json b/frontend/providers/workorder/public/locales/zh/common.json
new file mode 100644
index 000000000..f59b5f71c
--- /dev/null
+++ b/frontend/providers/workorder/public/locales/zh/common.json
@@ -0,0 +1,64 @@
+{
+ "file": {
+ "Uploading": "正在上传文件,进度: {{percent}}%",
+ "Upload Success": "文件上传成功"
+ },
+ "All": "全部",
+ "Total": "总数",
+ "Page": "页",
+ "Creating": "创建中",
+ "Failed": "有异常",
+ "Name": "名字",
+ "Cancel": "取消",
+ "Confirm": "确认",
+ "New Order": "新建工单",
+ "Submit Order": "提交工单",
+ "Problem Description": "问题描述",
+ "appendix": "附件",
+ "Description": "详细描述",
+ "Type": "类型",
+ "Order List": "工单列表",
+ "describe your problem": "描述您的问题",
+ "Are you sure you want to submit this work order": "确定提交此工单吗",
+ "Order Name": "工单号",
+ "Operation": "操作",
+ "Question Type": "问题类型",
+ "Status": "状态",
+ "Create Time": "创建时间",
+ "Details": "详情",
+ "Close": "关闭",
+ "Delete": "删除",
+ "Order Detail": "工单详情",
+ "Order Conversation": "工单对话",
+ "Send": "发送",
+ "Order Empty": "暂无工单",
+ "Upload File": "上传文件",
+ "None": "暂无",
+ "Processing": "进行中",
+ "Completed": "已完成",
+ "Prompt": "提示",
+ "File upload failed": "文件上传失败",
+ "Pending": "待处理",
+ "Submit Error": "提交错误",
+ "Deleted": "已删除",
+ "user info": "用户信息",
+ "Switch to manual": "转人工客服",
+ "network anomaly": "网络异常",
+ "Notification SwitchToManual Tips": "已转人工处理,请耐心等待。",
+ "please wait for the end": "正在聊天中...请等待结束",
+ "Request exceeds limit": "请求超出限制",
+ "applaunchpad": "应用管理",
+ "costcenter": "费用中心",
+ "template": "应用市场",
+ "dbprovider": "数据库",
+ "cronjob": "定时任务",
+ "cloudserver": "云主机",
+ "objectstorage": "对象存储",
+ "functionserver": "云开发",
+ "send": "发送",
+ "newline": "换行",
+ "Copy": "复制",
+ "api is error": "api is error",
+ "region": "可用区",
+ "userId": "用户ID"
+}
diff --git a/frontend/providers/workorder/public/logo.svg b/frontend/providers/workorder/public/logo.svg
new file mode 100644
index 000000000..c72292f49
--- /dev/null
+++ b/frontend/providers/workorder/public/logo.svg
@@ -0,0 +1,9 @@
+
\ No newline at end of file
diff --git a/frontend/providers/workorder/src/api/platform.ts b/frontend/providers/workorder/src/api/platform.ts
new file mode 100644
index 000000000..3a6aed711
--- /dev/null
+++ b/frontend/providers/workorder/src/api/platform.ts
@@ -0,0 +1,35 @@
+import { FeishuNotificationParams } from '@/pages/api/platform/feishu';
+import { SystemEnvResponse } from '@/pages/api/platform/getEnv';
+import { GET, POST } from '@/services/request';
+import { AxiosProgressEvent } from 'axios';
+
+export const getSystemEnv = () => GET('/api/platform/getEnv');
+
+export const uploadFile = (
+ data: FormData,
+ onUploadProgress?: (progressEvent: AxiosProgressEvent) => void
+) => {
+ return POST<
+ {
+ originalName: string;
+ fileName: string;
+ }[]
+ >('/api/minio/upload', data, {
+ headers: {
+ 'Content-Type': 'multipart/form-data'
+ },
+ timeout: 3 * 60 * 1000,
+ onUploadProgress
+ });
+};
+
+export const getFileUrl = ({ fileName }: { fileName: string }) => {
+ return GET('/api/minio/getUrl', { fileName });
+};
+
+export const deleteFileByName = ({ fileName }: { fileName: string }) => {
+ return GET('/api/minio/delete', { fileName });
+};
+
+export const FeishuNotification = (payload: FeishuNotificationParams) =>
+ POST('/api/platform/feishu', payload);
diff --git a/frontend/providers/workorder/src/api/user.ts b/frontend/providers/workorder/src/api/user.ts
new file mode 100644
index 000000000..99137f9c4
--- /dev/null
+++ b/frontend/providers/workorder/src/api/user.ts
@@ -0,0 +1,9 @@
+import { GET, POST } from '@/services/request';
+import { RegionDB } from '@/types/region';
+import { AppSession, UserDB } from '@/types/user';
+
+export const findUserById = ({ orderId }: { orderId: string }) =>
+ GET<{ user: UserDB; regionInfo: RegionDB }>('/api/auth/findById', { orderId: orderId });
+
+export const AuthByDesktopSession = (payload: { token: string }) =>
+ POST('/api/auth/desktop', payload);
diff --git a/frontend/providers/workorder/src/api/workorder.ts b/frontend/providers/workorder/src/api/workorder.ts
new file mode 100644
index 000000000..038ab4565
--- /dev/null
+++ b/frontend/providers/workorder/src/api/workorder.ts
@@ -0,0 +1,30 @@
+import { CreateWorkOrderParams } from '@/pages/api/workorder/create';
+import { GET, POST } from '@/services/request';
+import { WorkOrderDB, WorkOrderStatus, WorkOrderType } from '@/types/workorder';
+
+export const createWorkOrder = (payload: CreateWorkOrderParams) =>
+ POST<{ orderId: string }>('/api/workorder/create', payload);
+
+export const getWorkOrderList = (payload: {
+ page: number;
+ pageSize: number;
+ orderType?: WorkOrderType;
+ orderStatus?: WorkOrderStatus;
+ startTime?: Date;
+ endTime?: Date;
+}) => POST<{ totalCount: number; orders: WorkOrderDB[] }>('/api/workorder/listByUser', payload);
+
+export const getWorkOrderById = (payload: { orderId: string }) =>
+ GET('/api/workorder/getByID', payload);
+
+export const delWorkOrderById = (payload: { orderId: string }) =>
+ GET('/api/workorder/delete', payload);
+
+export const updateWorkOrderById = (payload: { updates: Partial; orderId: string }) =>
+ POST('/api/workorder/update', payload);
+
+export const updateWorkOrderDialogById = (payload: {
+ orderId: string;
+ content: string;
+ isRobot?: boolean;
+}) => POST('/api/workorder/updateDialog', payload);
diff --git a/frontend/providers/workorder/src/components/AppStatusTag/index.tsx b/frontend/providers/workorder/src/components/AppStatusTag/index.tsx
new file mode 100644
index 000000000..9bd941e5a
--- /dev/null
+++ b/frontend/providers/workorder/src/components/AppStatusTag/index.tsx
@@ -0,0 +1,38 @@
+import { StatusMap } from '@/constants/workorder';
+import { WorkOrderStatus } from '@/types/workorder';
+import { Box, Flex } from '@chakra-ui/react';
+import { useTranslation } from 'next-i18next';
+
+const AppStatusTag = ({
+ status,
+ showBorder = false
+}: {
+ status: WorkOrderStatus;
+ showBorder: boolean;
+}) => {
+ const { t } = useTranslation();
+ const statusMap = StatusMap[status];
+
+ return (
+
+
+
+ {t(statusMap.label)}
+
+
+ );
+};
+
+export default AppStatusTag;
diff --git a/frontend/providers/workorder/src/components/FileSelect/index.tsx b/frontend/providers/workorder/src/components/FileSelect/index.tsx
new file mode 100644
index 000000000..3f1a7fec5
--- /dev/null
+++ b/frontend/providers/workorder/src/components/FileSelect/index.tsx
@@ -0,0 +1,255 @@
+import { useLoading } from '@/hooks/useLoading';
+import { useSelectFile } from '@/hooks/useSelectFile';
+import { useToast } from '@/hooks/useToast';
+import { Box, Center, Flex, Icon, Text, type BoxProps } from '@chakra-ui/react';
+import { customAlphabet } from 'nanoid';
+import { useTranslation } from 'next-i18next';
+import { DragEvent, useCallback, useState } from 'react';
+
+const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 12);
+
+export const FileImgs = [
+ {
+ reg: /\.yaml$/i,
+ src: (
+
+
+
+ )
+ },
+ {
+ reg: /\.(svg|png|jpg|jpeg|gif|bmp)$/i,
+ src: (
+
+
+
+
+
+ )
+ }
+];
+
+export type FileItemType = {
+ id: string;
+ filename: string;
+ text?: string;
+ file: File;
+ icon: JSX.Element;
+};
+interface Props extends BoxProps {
+ fileExtension: string;
+ files: FileItemType[];
+ setFiles: React.Dispatch>;
+}
+
+const FileSelect = ({ fileExtension, setFiles, files, ...BoxStyle }: Props) => {
+ const { Loading: FileSelectLoading } = useLoading();
+ const { t } = useTranslation();
+ const { toast } = useToast();
+ const [isDragging, setIsDragging] = useState(false);
+ const [selecting, setSelecting] = useState(false);
+
+ const { File, onOpen } = useSelectFile({
+ fileType: fileExtension,
+ multiple: true
+ });
+
+ const onSelectFile = useCallback(
+ async (files: File[]) => {
+ setSelecting(true);
+ try {
+ // Parse file by file
+ let promise = Promise.resolve([]);
+ files.forEach((file) => {
+ promise = promise.then(async (result) => {
+ let icon = FileImgs.find((item) => item.reg.test(file.name))?.src;
+ if (!icon) {
+ icon = FileImgs[0].src;
+ }
+ const fileItem: FileItemType = {
+ id: nanoid(),
+ filename: file.name,
+ icon,
+ file: file
+ };
+ return [...result, fileItem];
+ });
+ });
+ const chunkFiles = await promise;
+ console.log(chunkFiles);
+
+ setFiles(chunkFiles);
+ } catch (error: any) {
+ console.log(error);
+ toast({
+ title: typeof error === 'string' ? error : '解析文件失败',
+ status: 'error'
+ });
+ }
+ setSelecting(false);
+ },
+ [setFiles, toast]
+ );
+
+ const handleDragEnter = (e: DragEvent) => {
+ e.preventDefault();
+ setIsDragging(true);
+ };
+
+ const handleDragLeave = (e: DragEvent) => {
+ e.preventDefault();
+ setIsDragging(false);
+ };
+
+ const handleDrop = useCallback(
+ async (e: DragEvent) => {
+ e.preventDefault();
+ setIsDragging(false);
+ const items = e.dataTransfer.items;
+ const fileList: File[] = [];
+
+ if (e.dataTransfer.items.length <= 1) {
+ const traverseFileTree = async (item: any) => {
+ return new Promise((resolve, reject) => {
+ if (item.isFile) {
+ item.file((file: File) => {
+ fileList.push(file);
+ resolve();
+ });
+ } else if (item.isDirectory) {
+ const dirReader = item.createReader();
+ dirReader.readEntries(async (entries: any[]) => {
+ for (let i = 0; i < entries.length; i++) {
+ await traverseFileTree(entries[i]);
+ }
+ resolve();
+ });
+ }
+ });
+ };
+ for (let i = 0; i < items.length; i++) {
+ const item = items[i].webkitGetAsEntry();
+ if (item) {
+ await traverseFileTree(item);
+ }
+ }
+ } else {
+ const files = Array.from(e.dataTransfer.files);
+ let isErr = files.some((item) => item.type === '');
+ if (isErr) {
+ return toast({
+ title: t('file.upload error description'),
+ status: 'error'
+ });
+ }
+ for (let i = 0; i < files.length; i++) {
+ fileList.push(files[i]);
+ }
+ }
+ onSelectFile(fileList);
+ },
+ [onSelectFile, t, toast]
+ );
+
+ return (
+
+
+
+
+
+
+ {t('Upload File')}
+
+ {/*
+ {files.map((item) => (
+
+ {item.icon}
+ {item.filename}
+ {
+ setFiles((state) => state.filter((file) => file.id !== item.id));
+ }}
+ >
+
+
+
+ ))}
+ */}
+
+
+
+
+
+ {files.map((item) => (
+
+
+ {item.icon}
+
+ {item.filename}
+ {
+ setFiles((state) => state.filter((file) => file.id !== item.id));
+ }}
+ >
+
+
+
+ ))}
+
+
+ );
+};
+
+export default FileSelect;
diff --git a/frontend/providers/workorder/src/components/Icon/icons/analyze.svg b/frontend/providers/workorder/src/components/Icon/icons/analyze.svg
new file mode 100644
index 000000000..503178647
--- /dev/null
+++ b/frontend/providers/workorder/src/components/Icon/icons/analyze.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/providers/workorder/src/components/Icon/icons/arrowLeft.svg b/frontend/providers/workorder/src/components/Icon/icons/arrowLeft.svg
new file mode 100644
index 000000000..7905b5380
--- /dev/null
+++ b/frontend/providers/workorder/src/components/Icon/icons/arrowLeft.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/providers/workorder/src/components/Icon/icons/change.svg b/frontend/providers/workorder/src/components/Icon/icons/change.svg
new file mode 100644
index 000000000..d81483220
--- /dev/null
+++ b/frontend/providers/workorder/src/components/Icon/icons/change.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/providers/workorder/src/components/Icon/icons/close.svg b/frontend/providers/workorder/src/components/Icon/icons/close.svg
new file mode 100644
index 000000000..62fb9a2a8
--- /dev/null
+++ b/frontend/providers/workorder/src/components/Icon/icons/close.svg
@@ -0,0 +1,3 @@
+
\ No newline at end of file
diff --git a/frontend/providers/workorder/src/components/Icon/icons/connection.svg b/frontend/providers/workorder/src/components/Icon/icons/connection.svg
new file mode 100644
index 000000000..393e9e2eb
--- /dev/null
+++ b/frontend/providers/workorder/src/components/Icon/icons/connection.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/providers/workorder/src/components/Icon/icons/continue.svg b/frontend/providers/workorder/src/components/Icon/icons/continue.svg
new file mode 100644
index 000000000..998357d4a
--- /dev/null
+++ b/frontend/providers/workorder/src/components/Icon/icons/continue.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/providers/workorder/src/components/Icon/icons/copy.svg b/frontend/providers/workorder/src/components/Icon/icons/copy.svg
new file mode 100644
index 000000000..bfd38df8e
--- /dev/null
+++ b/frontend/providers/workorder/src/components/Icon/icons/copy.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/providers/workorder/src/components/Icon/icons/defaultAvator.svg b/frontend/providers/workorder/src/components/Icon/icons/defaultAvator.svg
new file mode 100644
index 000000000..9f8212841
--- /dev/null
+++ b/frontend/providers/workorder/src/components/Icon/icons/defaultAvator.svg
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/frontend/providers/workorder/src/components/Icon/icons/delete.svg b/frontend/providers/workorder/src/components/Icon/icons/delete.svg
new file mode 100644
index 000000000..11907eec5
--- /dev/null
+++ b/frontend/providers/workorder/src/components/Icon/icons/delete.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/providers/workorder/src/components/Icon/icons/detail.svg b/frontend/providers/workorder/src/components/Icon/icons/detail.svg
new file mode 100644
index 000000000..380ce9fff
--- /dev/null
+++ b/frontend/providers/workorder/src/components/Icon/icons/detail.svg
@@ -0,0 +1,3 @@
+
\ No newline at end of file
diff --git a/frontend/providers/workorder/src/components/Icon/icons/download.svg b/frontend/providers/workorder/src/components/Icon/icons/download.svg
new file mode 100644
index 000000000..55919030d
--- /dev/null
+++ b/frontend/providers/workorder/src/components/Icon/icons/download.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/providers/workorder/src/components/Icon/icons/formInfo.svg b/frontend/providers/workorder/src/components/Icon/icons/formInfo.svg
new file mode 100644
index 000000000..164e7a19d
--- /dev/null
+++ b/frontend/providers/workorder/src/components/Icon/icons/formInfo.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/providers/workorder/src/components/Icon/icons/info.svg b/frontend/providers/workorder/src/components/Icon/icons/info.svg
new file mode 100644
index 000000000..5f81980ab
--- /dev/null
+++ b/frontend/providers/workorder/src/components/Icon/icons/info.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/providers/workorder/src/components/Icon/icons/link.svg b/frontend/providers/workorder/src/components/Icon/icons/link.svg
new file mode 100644
index 000000000..126255b73
--- /dev/null
+++ b/frontend/providers/workorder/src/components/Icon/icons/link.svg
@@ -0,0 +1,4 @@
+
\ No newline at end of file
diff --git a/frontend/providers/workorder/src/components/Icon/icons/log.svg b/frontend/providers/workorder/src/components/Icon/icons/log.svg
new file mode 100644
index 000000000..7dedd37e5
--- /dev/null
+++ b/frontend/providers/workorder/src/components/Icon/icons/log.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/providers/workorder/src/components/Icon/icons/logo.svg b/frontend/providers/workorder/src/components/Icon/icons/logo.svg
new file mode 100644
index 000000000..d826088c3
--- /dev/null
+++ b/frontend/providers/workorder/src/components/Icon/icons/logo.svg
@@ -0,0 +1,3 @@
+
diff --git a/frontend/providers/workorder/src/components/Icon/icons/more.svg b/frontend/providers/workorder/src/components/Icon/icons/more.svg
new file mode 100644
index 000000000..bc3ddc9e9
--- /dev/null
+++ b/frontend/providers/workorder/src/components/Icon/icons/more.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/providers/workorder/src/components/Icon/icons/noEvents.svg b/frontend/providers/workorder/src/components/Icon/icons/noEvents.svg
new file mode 100644
index 000000000..9665a7024
--- /dev/null
+++ b/frontend/providers/workorder/src/components/Icon/icons/noEvents.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/providers/workorder/src/components/Icon/icons/pause.svg b/frontend/providers/workorder/src/components/Icon/icons/pause.svg
new file mode 100644
index 000000000..8352927e1
--- /dev/null
+++ b/frontend/providers/workorder/src/components/Icon/icons/pause.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/providers/workorder/src/components/Icon/icons/plus.svg b/frontend/providers/workorder/src/components/Icon/icons/plus.svg
new file mode 100644
index 000000000..828a964b1
--- /dev/null
+++ b/frontend/providers/workorder/src/components/Icon/icons/plus.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/providers/workorder/src/components/Icon/icons/podList.svg b/frontend/providers/workorder/src/components/Icon/icons/podList.svg
new file mode 100644
index 000000000..6be85b62a
--- /dev/null
+++ b/frontend/providers/workorder/src/components/Icon/icons/podList.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/providers/workorder/src/components/Icon/icons/read.svg b/frontend/providers/workorder/src/components/Icon/icons/read.svg
new file mode 100644
index 000000000..3140dd585
--- /dev/null
+++ b/frontend/providers/workorder/src/components/Icon/icons/read.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/providers/workorder/src/components/Icon/icons/restart.svg b/frontend/providers/workorder/src/components/Icon/icons/restart.svg
new file mode 100644
index 000000000..de42eb256
--- /dev/null
+++ b/frontend/providers/workorder/src/components/Icon/icons/restart.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/providers/workorder/src/components/Icon/icons/restore.svg b/frontend/providers/workorder/src/components/Icon/icons/restore.svg
new file mode 100644
index 000000000..2e89f43b6
--- /dev/null
+++ b/frontend/providers/workorder/src/components/Icon/icons/restore.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/providers/workorder/src/components/Icon/icons/robot.svg b/frontend/providers/workorder/src/components/Icon/icons/robot.svg
new file mode 100644
index 000000000..f02495c3d
--- /dev/null
+++ b/frontend/providers/workorder/src/components/Icon/icons/robot.svg
@@ -0,0 +1,3 @@
+
\ No newline at end of file
diff --git a/frontend/providers/workorder/src/components/Icon/icons/sealosAvator.svg b/frontend/providers/workorder/src/components/Icon/icons/sealosAvator.svg
new file mode 100644
index 000000000..9c9f24a9b
--- /dev/null
+++ b/frontend/providers/workorder/src/components/Icon/icons/sealosAvator.svg
@@ -0,0 +1,6 @@
+
\ No newline at end of file
diff --git a/frontend/providers/workorder/src/components/Icon/icons/settings.svg b/frontend/providers/workorder/src/components/Icon/icons/settings.svg
new file mode 100644
index 000000000..3cd5c9e17
--- /dev/null
+++ b/frontend/providers/workorder/src/components/Icon/icons/settings.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/providers/workorder/src/components/Icon/icons/start.svg b/frontend/providers/workorder/src/components/Icon/icons/start.svg
new file mode 100644
index 000000000..8aa2f2981
--- /dev/null
+++ b/frontend/providers/workorder/src/components/Icon/icons/start.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/providers/workorder/src/components/Icon/icons/statusDetail.svg b/frontend/providers/workorder/src/components/Icon/icons/statusDetail.svg
new file mode 100644
index 000000000..9716b6471
--- /dev/null
+++ b/frontend/providers/workorder/src/components/Icon/icons/statusDetail.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/providers/workorder/src/components/Icon/icons/terminal.svg b/frontend/providers/workorder/src/components/Icon/icons/terminal.svg
new file mode 100644
index 000000000..db9147be9
--- /dev/null
+++ b/frontend/providers/workorder/src/components/Icon/icons/terminal.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/providers/workorder/src/components/Icon/icons/unread.svg b/frontend/providers/workorder/src/components/Icon/icons/unread.svg
new file mode 100644
index 000000000..29403d117
--- /dev/null
+++ b/frontend/providers/workorder/src/components/Icon/icons/unread.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/providers/workorder/src/components/Icon/icons/warning.svg b/frontend/providers/workorder/src/components/Icon/icons/warning.svg
new file mode 100644
index 000000000..104b2dba1
--- /dev/null
+++ b/frontend/providers/workorder/src/components/Icon/icons/warning.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/providers/workorder/src/components/Icon/index.tsx b/frontend/providers/workorder/src/components/Icon/index.tsx
new file mode 100644
index 000000000..9135d9fd1
--- /dev/null
+++ b/frontend/providers/workorder/src/components/Icon/index.tsx
@@ -0,0 +1,51 @@
+import React from 'react';
+import type { IconProps } from '@chakra-ui/react';
+import { Icon } from '@chakra-ui/react';
+
+const map = {
+ more: require('./icons/more.svg').default,
+ podList: require('./icons/podList.svg').default,
+ arrowLeft: require('./icons/arrowLeft.svg').default,
+ plus: require('./icons/plus.svg').default,
+ delete: require('./icons/delete.svg').default,
+ restart: require('./icons/restart.svg').default,
+ start: require('./icons/start.svg').default,
+ pause: require('./icons/pause.svg').default,
+ detail: require('./icons/detail.svg').default,
+ logo: require('./icons/logo.svg').default,
+ change: require('./icons/change.svg').default,
+ formInfo: require('./icons/formInfo.svg').default,
+ settings: require('./icons/settings.svg').default,
+ copy: require('./icons/copy.svg').default,
+ continue: require('./icons/continue.svg').default,
+ noEvents: require('./icons/noEvents.svg').default,
+ warning: require('./icons/warning.svg').default,
+ analyze: require('./icons/analyze.svg').default,
+ terminal: require('./icons/terminal.svg').default,
+ log: require('./icons/log.svg').default,
+ statusDetail: require('./icons/statusDetail.svg').default,
+ read: require('./icons/read.svg').default,
+ unread: require('./icons/unread.svg').default,
+ connection: require('./icons/connection.svg').default,
+ info: require('./icons/info.svg').default,
+ restore: require('./icons/restore.svg').default,
+ download: require('./icons/download.svg').default,
+ close: require('./icons/close.svg').default,
+ robot: require('./icons/robot.svg').default,
+ sealosAvator: require('./icons/sealosAvator.svg').default,
+ defaultAvator: require('./icons/defaultAvator.svg').default,
+ link: require('./icons/link.svg').default
+};
+
+const MyIcon = ({
+ name,
+ w = 'auto',
+ h = 'auto',
+ ...props
+}: { name: keyof typeof map } & IconProps) => {
+ return map[name] ? (
+
+ ) : null;
+};
+
+export default MyIcon;
diff --git a/frontend/providers/workorder/src/components/Markdown/CodeLight.tsx b/frontend/providers/workorder/src/components/Markdown/CodeLight.tsx
new file mode 100644
index 000000000..a1354b477
--- /dev/null
+++ b/frontend/providers/workorder/src/components/Markdown/CodeLight.tsx
@@ -0,0 +1,345 @@
+import React, { useMemo } from 'react';
+import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
+import { Box, Flex } from '@chakra-ui/react';
+import { useTranslation } from 'next-i18next';
+import { useCopyData } from '@/utils/tools';
+import MyIcon from '../Icon';
+
+const codeLight: { [key: string]: React.CSSProperties } = {
+ 'code[class*=language-]': {
+ color: '#d4d4d4',
+ textShadow: 'none',
+ direction: 'ltr',
+ textAlign: 'left',
+ whiteSpace: 'pre',
+ wordSpacing: 'normal',
+ wordBreak: 'normal',
+ lineHeight: '1.5',
+ MozTabSize: '4',
+ OTabSize: '4',
+ tabSize: '4',
+ WebkitHyphens: 'none',
+ MozHyphens: 'none',
+ msHyphens: 'none',
+ hyphens: 'none'
+ },
+ 'pre[class*=language-]': {
+ color: '#d4d4d4',
+ textShadow: 'none',
+ direction: 'ltr',
+ textAlign: 'left',
+ whiteSpace: 'pre',
+ wordSpacing: 'normal',
+ wordBreak: 'normal',
+ lineHeight: '1.5',
+ MozTabSize: '4',
+ OTabSize: '4',
+ tabSize: '4',
+ WebkitHyphens: 'none',
+ MozHyphens: 'none',
+ msHyphens: 'none',
+ hyphens: 'none',
+ padding: '1em',
+ margin: '.5em 0',
+ overflow: 'auto',
+ background: '#1e1e1e'
+ },
+ 'code[class*=language-] ::selection': {
+ textShadow: 'none',
+ background: '#264f78'
+ },
+ 'code[class*=language-]::selection': {
+ textShadow: 'none',
+ background: '#264f78'
+ },
+ 'pre[class*=language-] ::selection': {
+ textShadow: 'none',
+ background: '#264f78'
+ },
+ 'pre[class*=language-]::selection': {
+ textShadow: 'none',
+ background: '#264f78'
+ },
+ ':not(pre)>code[class*=language-]': {
+ padding: '.1em .3em',
+ borderRadius: '.3em',
+ color: '#db4c69',
+ background: '#1e1e1e'
+ },
+ '.namespace': {
+ opacity: '0.7'
+ },
+ 'doctype.doctype-tag': {
+ color: '#569cd6'
+ },
+ 'doctype.name': {
+ color: '#9cdcfe'
+ },
+ comment: {
+ color: '#6a9955'
+ },
+ prolog: {
+ color: '#6a9955'
+ },
+ '.language-html .language-css .token.punctuation': {
+ color: '#d4d4d4'
+ },
+ '.language-html .language-javascript .token.punctuation': {
+ color: '#d4d4d4'
+ },
+ punctuation: {
+ color: '#d4d4d4'
+ },
+ boolean: {
+ color: '#569cd6'
+ },
+ constant: {
+ color: '#9cdcfe'
+ },
+ inserted: {
+ color: '#b5cea8'
+ },
+ number: {
+ color: '#b5cea8'
+ },
+ property: {
+ color: '#9cdcfe'
+ },
+ symbol: {
+ color: '#b5cea8'
+ },
+ tag: {
+ color: '#569cd6'
+ },
+ unit: {
+ color: '#b5cea8'
+ },
+ 'attr-name': {
+ color: '#9cdcfe'
+ },
+ builtin: {
+ color: '#ce9178'
+ },
+ char: {
+ color: '#ce9178'
+ },
+ deleted: {
+ color: '#ce9178'
+ },
+ selector: {
+ color: '#d7ba7d'
+ },
+ string: {
+ color: '#ce9178'
+ },
+ '.language-css .token.string.url': {
+ textDecoration: 'underline'
+ },
+ entity: {
+ color: '#569cd6'
+ },
+ operator: {
+ color: '#d4d4d4'
+ },
+ 'operator.arrow': {
+ color: '#569cd6'
+ },
+ atrule: {
+ color: '#ce9178'
+ },
+ 'atrule.rule': {
+ color: '#c586c0'
+ },
+ 'atrule.url': {
+ color: '#9cdcfe'
+ },
+ 'atrule.url.function': {
+ color: '#dcdcaa'
+ },
+ 'atrule.url.punctuation': {
+ color: '#d4d4d4'
+ },
+ keyword: {
+ color: '#569cd6'
+ },
+ 'keyword.control-flow': {
+ color: '#c586c0'
+ },
+ 'keyword.module': {
+ color: '#c586c0'
+ },
+ function: {
+ color: '#dcdcaa'
+ },
+ 'function.maybe-class-name': {
+ color: '#dcdcaa'
+ },
+ regex: {
+ color: '#d16969'
+ },
+ important: {
+ color: '#569cd6'
+ },
+ italic: {
+ fontStyle: 'italic'
+ },
+ 'class-name': {
+ color: '#4ec9b0'
+ },
+ 'maybe-class-name': {
+ color: '#4ec9b0'
+ },
+ console: {
+ color: '#9cdcfe'
+ },
+ parameter: {
+ color: '#9cdcfe'
+ },
+ interpolation: {
+ color: '#9cdcfe'
+ },
+ 'punctuation.interpolation-punctuation': {
+ color: '#569cd6'
+ },
+ 'exports.maybe-class-name': {
+ color: '#9cdcfe'
+ },
+ 'imports.maybe-class-name': {
+ color: '#9cdcfe'
+ },
+ variable: {
+ color: '#9cdcfe'
+ },
+ escape: {
+ color: '#d7ba7d'
+ },
+ 'tag.punctuation': {
+ color: 'grey'
+ },
+ cdata: {
+ color: 'grey'
+ },
+ 'attr-value': {
+ color: '#ce9178'
+ },
+ 'attr-value.punctuation': {
+ color: '#ce9178'
+ },
+ 'attr-value.punctuation.attr-equals': {
+ color: '#d4d4d4'
+ },
+ namespace: {
+ color: '#4ec9b0'
+ },
+ 'code[class*=language-javascript]': {
+ color: '#9cdcfe'
+ },
+ 'code[class*=language-jsx]': {
+ color: '#9cdcfe'
+ },
+ 'code[class*=language-tsx]': {
+ color: '#9cdcfe'
+ },
+ 'code[class*=language-typescript]': {
+ color: '#9cdcfe'
+ },
+ 'pre[class*=language-javascript]': {
+ color: '#9cdcfe'
+ },
+ 'pre[class*=language-jsx]': {
+ color: '#9cdcfe'
+ },
+ 'pre[class*=language-tsx]': {
+ color: '#9cdcfe'
+ },
+ 'pre[class*=language-typescript]': {
+ color: '#9cdcfe'
+ },
+ 'code[class*=language-css]': {
+ color: '#ce9178'
+ },
+ 'pre[class*=language-css]': {
+ color: '#ce9178'
+ },
+ 'code[class*=language-html]': {
+ color: '#d4d4d4'
+ },
+ 'pre[class*=language-html]': {
+ color: '#d4d4d4'
+ },
+ '.language-regex .token.anchor': {
+ color: '#dcdcaa'
+ },
+ '.language-html .token.punctuation': {
+ color: 'grey'
+ },
+ 'pre[class*=language-]>code[class*=language-]': {
+ position: 'relative',
+ zIndex: '1'
+ },
+ '.line-highlight.line-highlight': {
+ background: '#f7ebc6',
+ boxShadow: 'inset 5px 0 0 #f7d87c',
+ zIndex: '0'
+ }
+};
+
+const CodeLight = ({
+ children,
+ className,
+ inline,
+ match
+}: {
+ children: React.ReactNode & React.ReactNode[];
+ className?: string;
+ inline?: boolean;
+ match: RegExpExecArray | null;
+}) => {
+ const { t } = useTranslation();
+ const { copyData } = useCopyData();
+
+ if (!inline) {
+ const codeBoxName = useMemo(() => {
+ const input = match?.['input'] || '';
+ if (!input) return match?.[1];
+
+ const splitInput = input.split('#');
+ return splitInput[1] || match?.[1];
+ }, [match]);
+
+ return (
+
+
+ {codeBoxName}
+ copyData(String(children))} alignItems={'center'}>
+
+ {t('Copy')}
+
+
+
+ {String(children).replace(/ /g, ' ')}
+
+
+ );
+ }
+
+ return {children};
+};
+
+export default React.memo(CodeLight);
diff --git a/frontend/providers/workorder/src/components/Markdown/img/Image.tsx b/frontend/providers/workorder/src/components/Markdown/img/Image.tsx
new file mode 100644
index 000000000..557082f79
--- /dev/null
+++ b/frontend/providers/workorder/src/components/Markdown/img/Image.tsx
@@ -0,0 +1,74 @@
+import {
+ Image,
+ Modal,
+ ModalCloseButton,
+ ModalContent,
+ ModalOverlay,
+ useDisclosure
+} from '@chakra-ui/react';
+import React, { WheelEventHandler, useState } from 'react';
+
+const MdImage = ({ src }: { src?: string }) => {
+ const [isLoading, setIsLoading] = useState(true);
+ const [succeed, setSucceed] = useState(false);
+ const { isOpen, onOpen, onClose } = useDisclosure();
+ const [scale, setScale] = useState(1);
+
+ const handleWheel: WheelEventHandler = (e) => {
+ setScale((prevScale) => {
+ const newScale = prevScale + e.deltaY * 0.5 * -0.01;
+ if (newScale < 0.5) return 0.5;
+ if (newScale > 10) return 10;
+ return newScale;
+ });
+ };
+
+ return (
+ <>
+ {
+ setIsLoading(false);
+ setSucceed(true);
+ }}
+ onError={() => setIsLoading(false)}
+ onClick={() => {
+ if (!succeed) return;
+ onOpen();
+ }}
+ />
+
+
+
+
+
+
+
+ >
+ );
+};
+
+export default React.memo(MdImage);
diff --git a/frontend/providers/workorder/src/components/Markdown/index.module.scss b/frontend/providers/workorder/src/components/Markdown/index.module.scss
new file mode 100644
index 000000000..684e63fde
--- /dev/null
+++ b/frontend/providers/workorder/src/components/Markdown/index.module.scss
@@ -0,0 +1,424 @@
+.waitingAnimation > :last-child::after {
+ display: inline-block;
+ content: '';
+ width: 3px;
+ height: 14px;
+ transform: translate(4px, 2px) scaleY(1.3);
+ background-color: var(--chakra-colors-primary-700);
+ animation: blink 0.6s infinite;
+}
+
+.animation {
+ height: 20px;
+
+ &::after {
+ display: inline-block;
+ content: '';
+ width: 3px;
+ height: 14px;
+ transform: translate(4px, 2px) scaleY(1.3);
+ background-color: var(--chakra-colors-primary-700);
+ animation: blink 0.6s infinite;
+ }
+}
+@keyframes blink {
+ from,
+ to {
+ opacity: 0;
+ }
+ 50% {
+ opacity: 1;
+ }
+}
+
+.markdown > *:first-child {
+ margin-top: 0 !important;
+}
+.markdown > *:last-child {
+ margin-bottom: 0 !important;
+}
+.markdown a.absent {
+ color: #cc0000;
+}
+.markdown a.anchor {
+ bottom: 0;
+ cursor: pointer;
+ display: block;
+ left: 0;
+ margin-left: -30px;
+ padding-left: 30px;
+ position: absolute;
+ top: 0;
+}
+.markdown h1,
+.markdown h2,
+.markdown h3,
+.markdown h4,
+.markdown h5,
+.markdown h6 {
+ cursor: text;
+ font-weight: bold;
+ margin: 10px 0;
+ padding: 0;
+ position: relative;
+}
+.markdown h1 .mini-icon-link,
+.markdown h2 .mini-icon-link,
+.markdown h3 .mini-icon-link,
+.markdown h4 .mini-icon-link,
+.markdown h5 .mini-icon-link,
+.markdown h6 .mini-icon-link {
+ display: none;
+}
+.markdown h1:hover a.anchor,
+.markdown h2:hover a.anchor,
+.markdown h3:hover a.anchor,
+.markdown h4:hover a.anchor,
+.markdown h5:hover a.anchor,
+.markdown h6:hover a.anchor {
+ line-height: 1;
+ margin-left: -22px;
+ padding-left: 0;
+ text-decoration: none;
+ top: 15%;
+}
+.markdown h1:hover a.anchor .mini-icon-link,
+.markdown h2:hover a.anchor .mini-icon-link,
+.markdown h3:hover a.anchor .mini-icon-link,
+.markdown h4:hover a.anchor .mini-icon-link,
+.markdown h5:hover a.anchor .mini-icon-link,
+.markdown h6:hover a.anchor .mini-icon-link {
+ display: inline-block;
+}
+.markdown h1 tt,
+.markdown h1 code,
+.markdown h2 tt,
+.markdown h2 code,
+.markdown h3 tt,
+.markdown h3 code,
+.markdown h4 tt,
+.markdown h4 code,
+.markdown h5 tt,
+.markdown h5 code,
+.markdown h6 tt,
+.markdown h6 code {
+ font-size: inherit;
+}
+.markdown h1 {
+ font-size: 28px;
+}
+.markdown h2 {
+ font-size: 24px;
+}
+.markdown h3 {
+ font-size: 18px;
+}
+.markdown h4 {
+ font-size: 16px;
+}
+.markdown h5 {
+ font-size: 14px;
+}
+.markdown h6 {
+ font-size: 12px;
+}
+.markdown p,
+.markdown blockquote,
+.markdown ul,
+.markdown ol,
+.markdown dl,
+.markdown table,
+.markdown pre {
+ margin: 14px 0;
+}
+.markdown > h2:first-child,
+.markdown > h1:first-child,
+.markdown > h1:first-child + h2,
+.markdown > h3:first-child,
+.markdown > h4:first-child,
+.markdown > h5:first-child,
+.markdown > h6:first-child {
+ margin-top: 0;
+ padding-top: 0;
+}
+.markdown a:first-child h1,
+.markdown a:first-child h2,
+.markdown a:first-child h3,
+.markdown a:first-child h4,
+.markdown a:first-child h5,
+.markdown a:first-child h6 {
+ margin-top: 0;
+ padding-top: 0;
+}
+.markdown h1 + p,
+.markdown h2 + p,
+.markdown h3 + p,
+.markdown h4 + p,
+.markdown h5 + p,
+.markdown h6 + p {
+ margin-top: 0;
+}
+.markdown li p.first {
+ display: inline-block;
+}
+.markdown ul,
+.markdown ol {
+ padding-left: 2em;
+}
+.markdown ul.no-list,
+.markdown ol.no-list {
+ list-style-type: none;
+ padding: 0;
+}
+.markdown ul li > *:first-child,
+.markdown ol li > *:first-child {
+ margin-top: 0;
+}
+.markdown ul,
+.markdown ol {
+ padding-left: 14px;
+}
+.markdown dl {
+ padding: 0;
+}
+.markdown dl dt {
+ font-size: 14px;
+ font-style: italic;
+ font-weight: bold;
+ margin: 15px 0 5px;
+ padding: 0;
+}
+.markdown dl dt:first-child {
+ padding: 0;
+}
+.markdown dl dt > *:first-child {
+ margin-top: 0;
+}
+.markdown dl dt > *:last-child {
+ margin-bottom: 0;
+}
+.markdown dl dd {
+ margin: 0 0 15px;
+ padding: 0 15px;
+}
+.markdown dl dd > *:first-child {
+ margin-top: 0;
+}
+.markdown dl dd > *:last-child {
+ margin-bottom: 0;
+}
+.markdown blockquote {
+ border-left: 4px solid #dddddd;
+ color: #777777;
+ padding: 0 15px;
+}
+.markdown blockquote > *:first-child {
+ margin-top: 0;
+}
+.markdown blockquote > *:last-child {
+ margin-bottom: 0;
+}
+.markdown table {
+ width: 100%;
+}
+.markdown table th {
+ font-weight: bold;
+}
+.markdown table th,
+.markdown table td {
+ padding: 6px 13px;
+}
+.markdown table tr {
+ background-color: #ffffff;
+}
+.markdown table tr:nth-child(2n) {
+ background-color: #f0f0f0;
+}
+.markdown img {
+ max-width: 100%;
+}
+.markdown span.frame {
+ display: block;
+ overflow: hidden;
+}
+.markdown span.frame > span {
+ border: 1px solid #dddddd;
+ display: block;
+ float: left;
+ margin: 13px 0 0;
+ overflow: hidden;
+ padding: 7px;
+ width: auto;
+}
+.markdown span.frame span img {
+ display: block;
+ float: left;
+}
+.markdown span.frame span span {
+ clear: both;
+ color: #333333;
+ display: block;
+ padding: 5px 0 0;
+}
+.markdown span.align-center {
+ clear: both;
+ display: block;
+ overflow: hidden;
+}
+.markdown span.align-center > span {
+ display: block;
+ margin: 13px auto 0;
+ overflow: hidden;
+ text-align: center;
+}
+.markdown span.align-center span img {
+ margin: 0 auto;
+ text-align: center;
+}
+.markdown span.align-right {
+ clear: both;
+ display: block;
+ overflow: hidden;
+}
+.markdown span.align-right > span {
+ display: block;
+ margin: 13px 0 0;
+ overflow: hidden;
+ text-align: right;
+}
+.markdown span.align-right span img {
+ margin: 0;
+ text-align: right;
+}
+.markdown span.float-left {
+ display: block;
+ float: left;
+ margin-right: 13px;
+ overflow: hidden;
+}
+.markdown span.float-left span {
+ margin: 13px 0 0;
+}
+.markdown span.float-right {
+ display: block;
+ float: right;
+ margin-left: 13px;
+ overflow: hidden;
+}
+.markdown span.float-right > span {
+ display: block;
+ margin: 13px auto 0;
+ overflow: hidden;
+ text-align: right;
+}
+.markdown code,
+.markdown tt {
+ border: 1px solid #dee0e2;
+ background-color: #f4f6f8;
+ border-radius: 3px;
+ margin: 0 2px;
+ padding: 0 5px;
+}
+.markdown pre > code {
+ background: none repeat scroll 0 0 transparent;
+ border: medium none;
+ margin: 0;
+ padding: 0;
+}
+.markdown .highlight pre,
+.markdown pre {
+ border: 1px solid #cccccc;
+ border-radius: 3px 3px 3px 3px;
+ font-size: max(0.9em, 14px);
+ line-height: 19px;
+ overflow: auto;
+ padding: 6px 10px;
+}
+.markdown pre code,
+.markdown pre tt {
+ background-color: transparent;
+ border: medium none;
+}
+.markdown hr {
+ margin: 10px 0;
+}
+.markdown {
+ tab-size: 4;
+ word-spacing: normal;
+ width: 100%;
+
+ * {
+ word-break: break-word;
+ }
+
+ pre {
+ display: block;
+ width: 100%;
+ padding: 15px;
+ margin: 0;
+ border: none;
+ border-radius: 0;
+ background-color: #292b33 !important;
+ overflow-x: auto;
+ color: #fff;
+ }
+
+ pre code {
+ background-color: #292b33 !important;
+ width: 100%;
+ }
+
+ a {
+ text-decoration: underline;
+ color: var(--chakra-colors-primary-700);
+ }
+
+ table {
+ border-collapse: separate;
+ border-spacing: 0px;
+ color: var(--chakra-colors-gray-700);
+
+ thead tr:first-child th {
+ border-bottom-width: 1px;
+ border-left-width: 1px;
+ border-top-width: 1px;
+ border-color: #ccc;
+ background-color: rgba(236, 236, 241, 0.2);
+ overflow: hidden;
+
+ &:first-child {
+ border-top-left-radius: 0.375rem;
+ }
+ &:last-child {
+ border-right-width: 1px;
+ border-top-right-radius: 0.375rem;
+ }
+ }
+
+ td {
+ border-bottom-width: 1px;
+ border-left-width: 1px;
+ border-color: #ccc;
+
+ &:last-of-type {
+ border-right-width: 1px;
+ }
+ }
+
+ tbody tr:last-child {
+ overflow: hidden;
+ td {
+ &:first-child {
+ border-bottom-left-radius: 0.375rem;
+ }
+ &:last-child {
+ border-bottom-right-radius: 0.375rem;
+ }
+ }
+ }
+ }
+}
+
+.mermaid {
+ overflow-x: auto;
+}
diff --git a/frontend/providers/workorder/src/components/Markdown/index.tsx b/frontend/providers/workorder/src/components/Markdown/index.tsx
new file mode 100644
index 000000000..54aebfeba
--- /dev/null
+++ b/frontend/providers/workorder/src/components/Markdown/index.tsx
@@ -0,0 +1,77 @@
+import dynamic from 'next/dynamic';
+import React, { useMemo } from 'react';
+import ReactMarkdown from 'react-markdown';
+import RemarkBreaks from 'remark-breaks';
+import RemarkGfm from 'remark-gfm';
+import styles from './index.module.scss';
+
+const CodeLight = dynamic(() => import('./CodeLight'));
+const MdImage = dynamic(() => import('./img/Image'));
+
+export enum CodeClassName {
+ guide = 'guide',
+ questionGuide = 'questionGuide',
+ mermaid = 'mermaid',
+ echarts = 'echarts',
+ quote = 'quote',
+ files = 'files'
+}
+
+const Markdown = ({
+ source = '',
+ showAnimation = false
+}: {
+ source?: string;
+ showAnimation?: boolean;
+}) => {
+ const components = useMemo(
+ () => ({
+ img: Image,
+ pre: 'div',
+ p: (pProps: any) => ,
+ code: Code
+ }),
+ []
+ );
+
+ const formatSource = source
+ .replace(/\\n/g, '\n ')
+ .replace(/(http[s]?:\/\/[^\s,。]+)([。,])/g, '$1 $2')
+ .replace(/\n*(\[QUOTE SIGN\]\(.*\))/g, '$1');
+
+ return (
+
+ {formatSource}
+
+ );
+};
+
+export default React.memo(Markdown);
+
+const Code = React.memo(function Code(e: any) {
+ const { inline, className, children } = e;
+
+ const match = /language-(\w+)/.exec(className || '');
+
+ const Component = useMemo(() => {
+ return (
+
+ {children}
+
+ );
+ }, [className, inline, match, children]);
+
+ return Component;
+});
+
+const Image = React.memo(function Image({ src }: { src?: string }) {
+ return ;
+});
diff --git a/frontend/providers/workorder/src/components/Menu/index.tsx b/frontend/providers/workorder/src/components/Menu/index.tsx
new file mode 100644
index 000000000..7740239c4
--- /dev/null
+++ b/frontend/providers/workorder/src/components/Menu/index.tsx
@@ -0,0 +1,50 @@
+import React from 'react';
+import { Menu, MenuList, MenuItem } from '@chakra-ui/react';
+
+interface Props {
+ width: number;
+ Button: React.ReactNode;
+ menuList: {
+ isActive?: boolean;
+ child: React.ReactNode;
+ onClick: () => void;
+ }[];
+}
+
+const MyMenu = ({ width, Button, menuList }: Props) => {
+ const menuItemStyles = {
+ borderRadius: 'sm',
+ py: 2,
+ display: 'flex',
+ alignItems: 'center',
+ _hover: {
+ backgroundColor: 'myWhite.600',
+ color: 'hover.blue'
+ }
+ };
+
+ return (
+
+ );
+};
+
+export default MyMenu;
diff --git a/frontend/providers/workorder/src/components/MonitorTabs/index.tsx b/frontend/providers/workorder/src/components/MonitorTabs/index.tsx
new file mode 100644
index 000000000..f037d7d6c
--- /dev/null
+++ b/frontend/providers/workorder/src/components/MonitorTabs/index.tsx
@@ -0,0 +1,97 @@
+import React, { useMemo } from 'react';
+import { Box, Flex, Grid } from '@chakra-ui/react';
+import type { GridProps } from '@chakra-ui/react';
+import { useTranslation } from 'next-i18next';
+
+// @ts-ignore
+interface Props extends GridProps {
+ list: { id: string; label: string }[];
+ activeId: string;
+ size?: 'sm' | 'md' | 'lg';
+ onChange: (id: string) => void;
+}
+
+const Tabs = ({ list, size = 'md', activeId, onChange, ...props }: Props) => {
+ const { t } = useTranslation();
+
+ const sizeMap = useMemo(() => {
+ switch (size) {
+ case 'sm':
+ return {
+ fontSize: 'sm',
+ outP: '3px',
+ inlineP: 1
+ };
+ case 'md':
+ return {
+ fontSize: 'md',
+ outP: '4px',
+ inlineP: 2
+ };
+ case 'lg':
+ return {
+ fontSize: 'lg',
+ outP: '5px',
+ inlineP: 3
+ };
+ }
+ }, [size]);
+
+ const activeIndex = useMemo(
+ () => list.findIndex((item) => item.id === activeId),
+ [activeId, list]
+ );
+
+ return (
+
+ {list.map((item, i) => (
+ {
+ if (activeId === item.id) return;
+ onChange(item.id);
+ }}
+ >
+ {t(item.label)}
+
+ ))}
+
+ );
+};
+
+export default Tabs;
diff --git a/frontend/providers/workorder/src/components/MyTooltip/index.tsx b/frontend/providers/workorder/src/components/MyTooltip/index.tsx
new file mode 100644
index 000000000..d51261c56
--- /dev/null
+++ b/frontend/providers/workorder/src/components/MyTooltip/index.tsx
@@ -0,0 +1,24 @@
+import React from 'react';
+import { Tooltip, TooltipProps } from '@chakra-ui/react';
+
+const MyTooltip = ({ children, ...props }: TooltipProps) => {
+ return (
+
+ {children}
+
+ );
+};
+
+export default MyTooltip;
diff --git a/frontend/providers/workorder/src/components/Pagination/index.tsx b/frontend/providers/workorder/src/components/Pagination/index.tsx
new file mode 100644
index 000000000..57650ae25
--- /dev/null
+++ b/frontend/providers/workorder/src/components/Pagination/index.tsx
@@ -0,0 +1,132 @@
+import { Flex, FlexProps, Icon, Text } from '@chakra-ui/react';
+import { Dispatch, SetStateAction, useState } from 'react';
+
+type PaginationProps = {
+ totalItems: number;
+ itemsPerPage: number;
+ currentPage: number;
+ setCurrentPage: Dispatch>;
+};
+
+export default function Pagination({
+ totalItems,
+ itemsPerPage,
+ currentPage = 1,
+ setCurrentPage
+}: PaginationProps) {
+ const totalPage = Math.ceil(totalItems / itemsPerPage);
+
+ const goToPage = (page: number) => {
+ if (page >= 1 && page <= totalPage) {
+ setCurrentPage(page);
+ }
+ };
+
+ const handlePrevPage = () => {
+ goToPage(currentPage - 1);
+ };
+
+ const handleNextPage = () => {
+ goToPage(currentPage + 1);
+ };
+
+ const buttonStyle: FlexProps = {
+ justifyContent: 'center',
+ alignItems: 'center',
+ w: '24px',
+ h: '24px',
+ borderRadius: '50%'
+ };
+
+ return (
+
+ Total:
+ {totalItems}
+
+ goToPage(1)}
+ {...buttonStyle}
+ {...{
+ bg: currentPage === 1 ? '#EDF0F2' : '#EDEFF1',
+ cursor: currentPage === 1 ? 'not-allowed' : 'pointer',
+ opacity: currentPage === 1 ? '0.5' : '1'
+ }}
+ >
+
+
+
+
+
+
+
+
+
+
+
+ {currentPage}/{totalPage}
+
+
+
+
+
+
+
+ goToPage(totalPage)}
+ >
+
+
+
+
+
+ {itemsPerPage}
+ /Page
+
+ );
+}
diff --git a/frontend/providers/workorder/src/components/RangeDate/index.tsx b/frontend/providers/workorder/src/components/RangeDate/index.tsx
new file mode 100644
index 000000000..c46b79ae2
--- /dev/null
+++ b/frontend/providers/workorder/src/components/RangeDate/index.tsx
@@ -0,0 +1,70 @@
+import {
+ Flex,
+ Input,
+ Popover,
+ PopoverTrigger,
+ Img,
+ PopoverContent,
+ Button,
+ Box,
+ useTheme
+} from '@chakra-ui/react';
+import { format, parse, isValid, isAfter, isBefore } from 'date-fns';
+import { useState, ChangeEventHandler, useEffect, useMemo } from 'react';
+import { DateRange, SelectRangeEventHandler, DayPicker } from 'react-day-picker';
+import { useQueryClient } from '@tanstack/react-query';
+import { CalendarIcon } from '@chakra-ui/icons';
+
+export default function RangeDate({
+ isDisabled = false,
+ defaultRange = { from: new Date(), to: new Date() }
+}: {
+ isDisabled?: boolean;
+ defaultRange?: DateRange;
+}) {
+ const [selectedRange, setSelectedRange] = useState(defaultRange);
+
+ const onClose = () => {};
+
+ return (
+
+
+
+
+
+ {
+ if (e) {
+ setSelectedRange(e);
+ } else {
+ setSelectedRange((state) => ({
+ from: state.from,
+ to: state.from
+ }));
+ }
+ }}
+ />
+
+
+ );
+}
diff --git a/frontend/providers/workorder/src/components/RangeDate/style.module.scss b/frontend/providers/workorder/src/components/RangeDate/style.module.scss
new file mode 100644
index 000000000..e69de29bb
diff --git a/frontend/providers/workorder/src/components/Select/index.tsx b/frontend/providers/workorder/src/components/Select/index.tsx
new file mode 100644
index 000000000..83f108226
--- /dev/null
+++ b/frontend/providers/workorder/src/components/Select/index.tsx
@@ -0,0 +1,117 @@
+import React from 'react';
+import { Menu, MenuButton, MenuList, MenuItem, Button, useDisclosure, Box } from '@chakra-ui/react';
+import type { ButtonProps } from '@chakra-ui/react';
+import { ChevronDownIcon } from '@chakra-ui/icons';
+import { useTranslation } from 'next-i18next';
+
+interface Props extends ButtonProps {
+ isDisabled?: boolean;
+ value?: string;
+ placeholder?: string;
+ list: {
+ label: string;
+ id: string;
+ }[];
+ width?: number | string;
+ icon?: React.ReactNode;
+ onchange?: (val: string) => void;
+}
+
+const MySelect = ({
+ placeholder,
+ isDisabled = false,
+ value,
+ width = 'auto',
+ list,
+ onchange,
+ icon = ,
+ ...props
+}: Props) => {
+ const { t } = useTranslation();
+
+ const menuItemStyles = {
+ borderRadius: 'sm',
+ py: 2,
+ display: 'flex',
+ alignItems: 'center',
+ transform: 'translate3d(0,0,0)',
+ cursor: 'pointer',
+ px: 3,
+ _hover: {
+ backgroundColor: 'myWhite.600'
+ }
+ };
+ const { isOpen, onOpen, onClose } = useDisclosure();
+
+ return (
+
+ );
+};
+
+export default React.memo(MySelect);
diff --git a/frontend/providers/workorder/src/components/SelectDateRange/index.tsx b/frontend/providers/workorder/src/components/SelectDateRange/index.tsx
new file mode 100644
index 000000000..635f189f4
--- /dev/null
+++ b/frontend/providers/workorder/src/components/SelectDateRange/index.tsx
@@ -0,0 +1,289 @@
+import {
+ Box,
+ Button,
+ Flex,
+ Icon,
+ Input,
+ Popover,
+ PopoverContent,
+ PopoverTrigger
+} from '@chakra-ui/react';
+import { endOfDay, format, isAfter, isBefore, isValid, parse, startOfDay } from 'date-fns';
+import { ChangeEventHandler, Dispatch, SetStateAction, useState } from 'react';
+import { DateRange, DayPicker, SelectRangeEventHandler } from 'react-day-picker';
+
+type SelectDateRangeProps = {
+ isDisabled?: boolean;
+ startTime: Date;
+ setStartTime: Dispatch>;
+ endTime: Date;
+ setEndTime: Dispatch>;
+};
+
+export default function SelectDateRange({
+ isDisabled,
+ startTime,
+ setStartTime,
+ endTime,
+ setEndTime
+}: SelectDateRangeProps) {
+ const initState = { from: startTime, to: endTime };
+
+ const [selectedRange, setSelectedRange] = useState(initState);
+ const [fromValue, setFromValue] = useState(format(initState.from, 'y-MM-dd'));
+ const [toValue, setToValue] = useState(format(initState.to, 'y-MM-dd'));
+ const [inputState, setInputState] = useState<0 | 1>(0);
+ const onClose = () => {
+ selectedRange?.from && setStartTime(startOfDay(selectedRange.from));
+ selectedRange?.to && setEndTime(endOfDay(selectedRange.to));
+ };
+
+ const handleFromChange: ChangeEventHandler = (e) => {
+ setFromValue(e.target.value);
+ const date = parse(e.target.value, 'y-MM-dd', new Date());
+ if (!isValid(date)) {
+ return setSelectedRange({ from: undefined, to: selectedRange?.to });
+ }
+
+ if (selectedRange?.to) {
+ if (isAfter(date, selectedRange.to)) {
+ setSelectedRange({ from: selectedRange.to, to: date });
+ } else {
+ setSelectedRange({ from: date, to: selectedRange?.to });
+ }
+ } else {
+ setSelectedRange({ from: date, to: date });
+ }
+ };
+
+ const handleToChange: ChangeEventHandler = (e) => {
+ setToValue(e.target.value);
+ const date = parse(e.target.value, 'y-MM-dd', new Date());
+
+ if (!isValid(date)) {
+ return setSelectedRange({ from: selectedRange?.from, to: undefined });
+ }
+ if (selectedRange?.from) {
+ if (isBefore(date, selectedRange.from)) {
+ setSelectedRange({ from: date, to: selectedRange.from });
+ } else {
+ setSelectedRange({ from: selectedRange?.from, to: date });
+ }
+ } else {
+ setSelectedRange({ from: date, to: date });
+ }
+ };
+
+ const handleRangeSelect: SelectRangeEventHandler = (range: DateRange | undefined) => {
+ if (range) {
+ let { from, to } = range;
+ if (inputState === 0) {
+ if (from === selectedRange?.from) {
+ from = to;
+ } else {
+ to = from;
+ }
+ setInputState(1);
+ } else {
+ setInputState(0);
+ }
+ setSelectedRange({
+ from,
+ to
+ });
+ if (from) {
+ setFromValue(format(from, 'y-MM-dd'));
+ } else {
+ setFromValue('');
+ }
+ if (to) {
+ setToValue(format(to, 'y-MM-dd'));
+ } else {
+ setToValue(from ? format(from, 'y-MM-dd') : '');
+ }
+ } else {
+ if (fromValue && selectedRange?.from) {
+ setToValue(fromValue);
+ setSelectedRange({
+ ...selectedRange,
+ to: selectedRange.from
+ });
+ setInputState(1);
+ }
+ }
+ };
+
+ const handleRangeSelectFrom: SelectRangeEventHandler = (range: DateRange | undefined) => {
+ if (range) {
+ let { from, to } = range;
+ if (selectedRange?.to) {
+ if (from) {
+ if (!to) {
+ to = from;
+ } else if (from === selectedRange?.from) {
+ from = to;
+ to = selectedRange.to;
+ }
+ if (isBefore(from, selectedRange.to)) {
+ setSelectedRange({
+ ...selectedRange,
+ from
+ });
+ setFromValue(format(from, 'y-MM-dd'));
+ }
+ }
+ }
+ }
+ };
+
+ const handleRangeSelectTo: SelectRangeEventHandler = (range: DateRange | undefined) => {
+ console.log(range, selectedRange);
+ if (range) {
+ let { from, to } = range;
+ if (selectedRange?.from) {
+ if (to) {
+ if (!from) {
+ from = to;
+ } else if (to === selectedRange?.to) {
+ to = from;
+ from = selectedRange.from;
+ }
+ if (isAfter(to, selectedRange.from)) {
+ setSelectedRange({
+ ...selectedRange,
+ to
+ });
+ setToValue(format(to, 'y-MM-dd'));
+ }
+ }
+ }
+ } else {
+ if (fromValue && selectedRange?.from) {
+ setToValue(fromValue);
+ setSelectedRange({
+ ...selectedRange,
+ to: selectedRange.from
+ });
+ setInputState(1);
+ }
+ }
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+ {
+ setInputState(0);
+ onClose();
+ }}
+ >
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/frontend/providers/workorder/src/components/Table/index.tsx b/frontend/providers/workorder/src/components/Table/index.tsx
new file mode 100644
index 000000000..3914c96fc
--- /dev/null
+++ b/frontend/providers/workorder/src/components/Table/index.tsx
@@ -0,0 +1,71 @@
+import { BoxProps, Flex, FlexProps } from '@chakra-ui/react';
+import { useTranslation } from 'next-i18next';
+
+interface Props extends BoxProps {
+ columns: {
+ title: string;
+ dataIndex?: string;
+ key: string;
+ render?: (item: any) => JSX.Element;
+ ItemStyle?: FlexProps;
+ }[];
+ data: any[];
+}
+
+const Table = ({ columns, data }: Props) => {
+ const { t } = useTranslation();
+
+ return (
+ <>
+
+ {columns.map((col, i) => (
+
+ {t(col.title)}
+
+ ))}
+
+ {data.map((item: any, index1) => (
+
+ {columns.map((col, index2) => (
+
+ {col.render ? col.render(item) : col.dataIndex ? `${item[col.dataIndex]}` : ''}
+
+ ))}
+
+ ))}
+ >
+ );
+};
+
+export default Table;
diff --git a/frontend/providers/workorder/src/components/Tabs/index.tsx b/frontend/providers/workorder/src/components/Tabs/index.tsx
new file mode 100644
index 000000000..3a91b9402
--- /dev/null
+++ b/frontend/providers/workorder/src/components/Tabs/index.tsx
@@ -0,0 +1,94 @@
+import React, { useMemo } from 'react';
+import { Box, Grid, useBreakpointValue } from '@chakra-ui/react';
+import type { GridProps } from '@chakra-ui/react';
+import { useTranslation } from 'next-i18next';
+
+// @ts-ignore
+interface Props extends GridProps {
+ list: { id: string; label: string }[];
+ activeId: string;
+ size?: 'sm' | 'md' | 'lg';
+ onChange: (id: string) => void;
+}
+
+const Tabs = ({ list, size = 'md', activeId, onChange, ...props }: Props) => {
+ const { t } = useTranslation();
+ const pxValue = useBreakpointValue({ base: '14px', lg: '20px' });
+
+ const sizeMap = useMemo(() => {
+ switch (size) {
+ case 'sm':
+ return {
+ fontSize: 'sm',
+ outP: '3px',
+ inlineP: 1
+ };
+ case 'md':
+ return {
+ fontSize: 'md',
+ outP: '4px',
+ inlineP: '4px'
+ };
+ case 'lg':
+ return {
+ fontSize: 'lg',
+ outP: '5px',
+ inlineP: 3
+ };
+ }
+ }, [size]);
+ const activeIndex = useMemo(
+ () => list.findIndex((item) => item.id === activeId),
+ [activeId, list]
+ );
+ return (
+
+ {list.map((item, i) => (
+ {
+ if (activeId === item.id) return;
+ onChange(item.id);
+ }}
+ >
+ {t(item.label)}
+
+ ))}
+
+ );
+};
+
+export default Tabs;
diff --git a/frontend/providers/workorder/src/components/Tip/index.tsx b/frontend/providers/workorder/src/components/Tip/index.tsx
new file mode 100644
index 000000000..de7031bb4
--- /dev/null
+++ b/frontend/providers/workorder/src/components/Tip/index.tsx
@@ -0,0 +1,66 @@
+import React, { useMemo } from 'react';
+import { BoxProps, Flex, Box } from '@chakra-ui/react';
+import { useTranslation } from 'next-i18next';
+
+interface Props extends BoxProps {
+ text: string;
+ icon?: JSX.Element;
+ theme?: 'blue';
+ size?: 'sm' | 'md' | 'lg';
+}
+
+const Tip = ({ size = 'md', text, icon, theme, ...props }: Props) => {
+ const { t } = useTranslation();
+
+ const sizeMap = useMemo(() => {
+ switch (size) {
+ case 'sm':
+ return {
+ fontSize: '12px',
+ px: 3,
+ py: 1
+ };
+ case 'md':
+ return {
+ fontSize: '14px',
+ px: 4,
+ py: 2
+ };
+ case 'lg':
+ return {
+ fontSize: '16px',
+ px: 5,
+ py: 2
+ };
+ default:
+ return {};
+ }
+ }, [size]);
+ const themeMap = useMemo(() => {
+ switch (theme) {
+ case 'blue':
+ return {
+ color: '#0884DD',
+ bg: '#ECF8FF'
+ };
+ default:
+ return {
+ color: '#0884DD',
+ bg: '#ECF8FF'
+ };
+ }
+ }, [theme]);
+
+ return (
+
+ {icon ? (
+
+ {icon}
+
+ ) : null}
+ {t(text)}
+
+ );
+};
+
+export default Tip;
diff --git a/frontend/providers/workorder/src/constants/theme.ts b/frontend/providers/workorder/src/constants/theme.ts
new file mode 100644
index 000000000..3265f4726
--- /dev/null
+++ b/frontend/providers/workorder/src/constants/theme.ts
@@ -0,0 +1,334 @@
+import {
+ ComponentStyleConfig,
+ createMultiStyleConfigHelpers,
+ defineStyle,
+ defineStyleConfig,
+ extendTheme
+} from '@chakra-ui/react';
+// @ts-ignore
+import {
+ selectAnatomy,
+ switchAnatomy,
+ numberInputAnatomy,
+ checkboxAnatomy
+} from '@chakra-ui/anatomy';
+const { definePartsStyle: selectPart, defineMultiStyleConfig: selectMultiStyle } =
+ createMultiStyleConfigHelpers(selectAnatomy.keys);
+const { definePartsStyle: switchPart, defineMultiStyleConfig: switchMultiStyle } =
+ createMultiStyleConfigHelpers(switchAnatomy.keys);
+const { definePartsStyle: numInputPart, defineMultiStyleConfig: numInputMultiStyle } =
+ createMultiStyleConfigHelpers(numberInputAnatomy.keys);
+const { definePartsStyle: checkboxPart, defineMultiStyleConfig: checkboxStyle } =
+ createMultiStyleConfigHelpers(checkboxAnatomy.keys);
+
+const Button = defineStyleConfig({
+ baseStyle: {
+ _active: {
+ transform: 'scale(0.98)'
+ }
+ },
+ sizes: {
+ xs: {
+ fontSize: 'xs',
+ px: 3,
+ py: 0,
+ fontWeight: 'normal',
+ height: '22px',
+ borderRadius: '2px'
+ },
+ sm: {
+ fontSize: 'sm',
+ px: 3,
+ py: 0,
+ fontWeight: 'normal',
+ height: '26px',
+ borderRadius: '2px'
+ },
+ md: {
+ fontSize: 'md',
+ px: 6,
+ py: 0,
+ height: '34px',
+ fontWeight: 'normal',
+ borderRadius: '4px'
+ },
+ lg: {
+ fontSize: 'lg',
+ px: 8,
+ py: 0,
+ height: '42px',
+ fontWeight: 'normal',
+ borderRadius: '8px'
+ }
+ },
+ variants: {
+ primary: {
+ bg: 'myGray.900',
+ color: 'white',
+ _hover: {
+ bg: 'myGray.700 !important'
+ }
+ },
+ base: {
+ bg: 'myWhite.600',
+ color: 'myGray.900',
+ border: '1px solid',
+ borderColor: 'myGray.100',
+ _hover: {
+ color: 'hover.blue'
+ }
+ }
+ },
+ defaultProps: {
+ size: 'md',
+ // @ts-ignore
+ variant: 'outline'
+ }
+});
+
+const Input: ComponentStyleConfig = {
+ baseStyle: {},
+ variants: {
+ outline: {
+ field: {
+ bg: 'myWhite.300',
+ border: '1px solid',
+ borderRadius: 'base',
+ borderColor: 'myGray.200',
+ _focus: {
+ borderColor: 'myBlue.600',
+ boxShadow: '0px 0px 4px #A8DBFF',
+ bg: 'white'
+ },
+ _disabled: {
+ color: 'myGray.400',
+ bg: 'myWhite.300'
+ }
+ }
+ }
+ },
+ defaultProps: {
+ size: 'md',
+ variant: 'outline'
+ }
+};
+const NumberInput = numInputMultiStyle({
+ variants: {
+ outline: numInputPart({
+ field: {
+ bg: 'myWhite.300',
+ border: '1px solid',
+ borderRadius: 'base',
+ borderColor: 'myGray.200',
+ _focus: {
+ borderColor: 'myBlue.600 !important',
+ boxShadow: '0px 0px 4px #A8DBFF !important',
+ bg: 'transparent'
+ },
+ _disabled: {
+ color: 'myGray.400 !important',
+ bg: 'myWhite.300 !important'
+ }
+ },
+ stepper: {
+ bg: 'transparent',
+ border: 'none',
+ color: 'myGray.600',
+ _active: {
+ color: 'myBlue.600'
+ }
+ }
+ })
+ },
+ defaultProps: {
+ variant: 'outline'
+ }
+});
+
+const Textarea: ComponentStyleConfig = {
+ variants: {
+ outline: {
+ bg: 'myWhite.300',
+ border: '1px solid',
+ borderRadius: 'base',
+ borderColor: 'myGray.200',
+ _focus: {
+ borderColor: 'myBlue.600',
+ boxShadow: '0px 0px 4px #A8DBFF'
+ }
+ }
+ },
+
+ defaultProps: {
+ size: 'md',
+ variant: 'outline'
+ }
+};
+
+const Select = selectMultiStyle({
+ variants: {
+ outline: selectPart({
+ field: {
+ borderColor: 'myGray.100',
+
+ _focusWithin: {
+ boxShadow: '0px 0px 4px #A8DBFF',
+ borderColor: 'myBlue.600'
+ }
+ }
+ })
+ }
+});
+
+const Switch = switchMultiStyle({
+ baseStyle: switchPart({
+ track: {
+ bg: 'myGray.100',
+ _checked: {
+ bg: 'myGray.700'
+ }
+ }
+ }),
+ variants: {
+ deepLight: {
+ track: {
+ bg: 'myGray.200',
+ _checked: {
+ bg: 'myGray.700'
+ }
+ }
+ }
+ }
+});
+
+const Tooltip = defineStyleConfig({
+ baseStyle: {
+ p: 2,
+ bg: 'white',
+ color: 'blackAlpha.800',
+ borderRadius: '8px',
+ boxShadow: '1px 1px 7px rgba(0,0,0,0.2)'
+ }
+});
+
+const Checkbox = checkboxStyle({
+ baseStyle: checkboxPart({
+ control: defineStyle({
+ _checked: {
+ bg: 'black',
+ borderColor: 'black',
+ _hover: {
+ bg: 'black !important',
+ borderColor: 'black !important'
+ }
+ }
+ })
+ })
+});
+
+export const theme = extendTheme({
+ styles: {
+ global: {
+ 'html, body': {
+ color: 'myGray.900',
+ fontSize: 'md',
+ height: '100%',
+ fontWeight: 400
+ }
+ }
+ },
+ Sizes: {
+ xs: '10px',
+ sm: '12px',
+ md: '14px',
+ lg: '16px',
+ xl: '16px',
+ '2xl': '18px',
+ '3xl': '20px'
+ },
+ colors: {
+ divider: {
+ 100: '#E5E7E9'
+ },
+ hover: {
+ blue: '#0884DD',
+ iconBlue: '#219BF4'
+ },
+ myGray: {
+ 100: '#EFF0F1',
+ 200: '#DEE0E2',
+ 300: '#BDC1C5',
+ 400: '#9CA2A8',
+ 500: '#7B838B',
+ 600: '#5A646E',
+ 700: '#485058',
+ 800: '#363C42',
+ 900: '#24282C',
+ 1000: '#121416'
+ },
+ myWhite: {
+ 100: '#FEFEFE',
+ 200: '#FDFDFE',
+ 300: '#FBFBFC',
+ 400: '#F8FAFB',
+ 500: '#F6F8F9',
+ 600: '#F4F6F8',
+ 700: '#C3C5C6',
+ 800: '#929495',
+ 900: '#626263',
+ 1000: '#313132'
+ },
+ myBlue: {
+ 100: '#EBF7FD',
+ 200: '#D7EFFC',
+ 300: '#AFDEF9',
+ 400: '#86CEF5',
+ 500: '#5EBDF2',
+ 600: '#36ADEF',
+ 700: '#2B8ABF',
+ 800: '#20688F',
+ 900: '#164560',
+ 1000: '#0B2330'
+ },
+ myRed: {
+ 100: '#FFEBED',
+ 200: '#FFD6DB',
+ 300: '#FFADB7',
+ 400: '#FF8492',
+ 500: '#FF5B6E',
+ 600: '#FF324A',
+ 700: '#CC283B',
+ 800: '#991E2C',
+ 900: '#66141E',
+ 1000: '#330A0F'
+ }
+ },
+ borders: {
+ sm: '1px solid #EFF0F1',
+ base: '1px solid #DEE0E2',
+ md: '1px solid #BDC1C5'
+ },
+ shadows: {
+ outline: ''
+ },
+ radii: {
+ xs: '1px',
+ sm: '2px',
+ base: '2px',
+ md: '4px',
+ lg: '6px'
+ },
+ fontWeights: {
+ bold: 500
+ },
+ components: {
+ Button,
+ Input,
+ Tooltip,
+ Select,
+ Switch,
+ Textarea,
+ NumberInput,
+ Checkbox
+ }
+});
diff --git a/frontend/providers/workorder/src/constants/workorder.ts b/frontend/providers/workorder/src/constants/workorder.ts
new file mode 100644
index 000000000..581a58a4d
--- /dev/null
+++ b/frontend/providers/workorder/src/constants/workorder.ts
@@ -0,0 +1,85 @@
+import { WorkOrderStatus, WorkOrderType } from '@/types/workorder';
+
+export const OrderTypeList: {
+ id: WorkOrderType;
+ label: string;
+}[] = [
+ {
+ id: WorkOrderType.AppLaunchpad,
+ label: 'applaunchpad'
+ },
+ {
+ id: WorkOrderType.CostCenter,
+ label: 'costcenter'
+ },
+ {
+ id: WorkOrderType.Template,
+ label: 'template'
+ },
+ {
+ id: WorkOrderType.DBProvider,
+ label: 'dbprovider'
+ },
+ {
+ id: WorkOrderType.CronJob,
+ label: 'cronjob'
+ },
+ {
+ id: WorkOrderType.CloudServer,
+ label: 'cloudserver'
+ },
+ {
+ id: WorkOrderType.ObjectStorage,
+ label: 'objectstorage'
+ },
+ {
+ id: WorkOrderType.FunctionServer,
+ label: 'functionserver'
+ }
+];
+
+export const StatusMap: {
+ [key in WorkOrderStatus]: {
+ label: string;
+ value: WorkOrderStatus;
+ color: string;
+ backgroundColor: string;
+ dotColor: string;
+ };
+} = {
+ all: {
+ label: 'Pending',
+ value: WorkOrderStatus.All,
+ color: '#FB7C3C',
+ backgroundColor: '#FFF2EC',
+ dotColor: '#FB7C3C'
+ },
+ pending: {
+ label: 'Pending',
+ value: WorkOrderStatus.Pending,
+ color: '#FB7C3C',
+ backgroundColor: '#FFF2EC',
+ dotColor: '#FB7C3C'
+ },
+ processing: {
+ label: 'Processing',
+ value: WorkOrderStatus.Processing,
+ color: '#219BF4',
+ backgroundColor: '#EBF7FD',
+ dotColor: '#219BF4'
+ },
+ completed: {
+ label: 'Completed',
+ value: WorkOrderStatus.Completed,
+ color: '#787A90',
+ backgroundColor: '#F5F5F8',
+ dotColor: '#787A90'
+ },
+ deleted: {
+ label: 'Deleted',
+ value: WorkOrderStatus.Deleted,
+ color: '#FF5B6E',
+ backgroundColor: '#FFEBED',
+ dotColor: '#FF5B6E'
+ }
+};
diff --git a/frontend/providers/workorder/src/hooks/useConfirm.tsx b/frontend/providers/workorder/src/hooks/useConfirm.tsx
new file mode 100644
index 000000000..a08a54b83
--- /dev/null
+++ b/frontend/providers/workorder/src/hooks/useConfirm.tsx
@@ -0,0 +1,79 @@
+import { useCallback, useRef } from 'react';
+import {
+ AlertDialog,
+ AlertDialogBody,
+ AlertDialogFooter,
+ AlertDialogHeader,
+ AlertDialogContent,
+ AlertDialogOverlay,
+ useDisclosure,
+ Button
+} from '@chakra-ui/react';
+import { useTranslation } from 'next-i18next';
+
+export const useConfirm = ({
+ title = 'Prompt',
+ content,
+ confirmText = 'Confirm'
+}: {
+ title?: string;
+ content: string;
+ confirmText?: string;
+}) => {
+ const { isOpen, onOpen, onClose } = useDisclosure();
+ const { t } = useTranslation();
+ const cancelRef = useRef(null);
+ const confirmCb = useRef();
+ const cancelCb = useRef();
+
+ return {
+ openConfirm: useCallback(
+ (confirm?: any, cancel?: any) => {
+ return function () {
+ onOpen();
+ confirmCb.current = confirm;
+ cancelCb.current = cancel;
+ };
+ },
+ [onOpen]
+ ),
+ ConfirmChild: useCallback(
+ () => (
+
+
+
+
+ {t(title)}
+
+
+ {content}
+
+
+
+
+
+
+
+
+ ),
+ [confirmText, content, isOpen, onClose, t, title]
+ )
+ };
+};
diff --git a/frontend/providers/workorder/src/hooks/useLoading.tsx b/frontend/providers/workorder/src/hooks/useLoading.tsx
new file mode 100644
index 000000000..f00dac51b
--- /dev/null
+++ b/frontend/providers/workorder/src/hooks/useLoading.tsx
@@ -0,0 +1,34 @@
+import { useState, useCallback } from 'react';
+import { Spinner, Flex } from '@chakra-ui/react';
+
+export const useLoading = (props?: { defaultLoading: boolean }) => {
+ const [isLoading, setIsLoading] = useState(props?.defaultLoading || false);
+
+ const Loading = useCallback(
+ ({ loading, fixed = true }: { loading?: boolean; fixed?: boolean }): JSX.Element | null => {
+ return (
+
+
+
+ );
+ },
+ [isLoading]
+ );
+
+ return {
+ isLoading,
+ setIsLoading,
+ Loading
+ };
+};
diff --git a/frontend/providers/workorder/src/hooks/useScreen.ts b/frontend/providers/workorder/src/hooks/useScreen.ts
new file mode 100644
index 000000000..79af2c568
--- /dev/null
+++ b/frontend/providers/workorder/src/hooks/useScreen.ts
@@ -0,0 +1,25 @@
+import { useState, useEffect } from 'react';
+import { throttle } from 'lodash';
+
+export function useScreen() {
+ const [screenWidth, setScreenWidth] = useState(1440);
+
+ useEffect(() => {
+ const resize = throttle(() => {
+ const w = document.documentElement.clientWidth;
+ setScreenWidth(w);
+ }, 300);
+ window.addEventListener('resize', resize);
+
+ const w = document.documentElement.clientWidth;
+ setScreenWidth(w);
+
+ return () => {
+ window.removeEventListener('resize', resize);
+ };
+ }, []);
+
+ return {
+ screenWidth
+ };
+}
diff --git a/frontend/providers/workorder/src/hooks/useSelectFile.tsx b/frontend/providers/workorder/src/hooks/useSelectFile.tsx
new file mode 100644
index 000000000..9570cc642
--- /dev/null
+++ b/frontend/providers/workorder/src/hooks/useSelectFile.tsx
@@ -0,0 +1,44 @@
+import React, { useRef, useCallback } from 'react';
+import { Box } from '@chakra-ui/react';
+import { useToast } from './useToast';
+import { useTranslation } from 'react-i18next';
+
+export const useSelectFile = (props?: { fileType?: string; multiple?: boolean }) => {
+ const { t } = useTranslation();
+ const { fileType = '*', multiple = false } = props || {};
+ const { toast } = useToast();
+ const SelectFileDom = useRef(null);
+
+ const File = useCallback(
+ ({ onSelect }: { onSelect: (e: File[]) => void }) => (
+
+ {
+ if (!e.target.files || e.target.files?.length === 0) return;
+ if (e.target.files.length > 10) {
+ return toast({
+ status: 'warning',
+ title: t('file.Select a maximum of 10 files')
+ });
+ }
+ onSelect(Array.from(e.target.files));
+ }}
+ />
+
+ ),
+ [fileType, multiple, t, toast]
+ );
+
+ const onOpen = useCallback(() => {
+ SelectFileDom.current && SelectFileDom.current.click();
+ }, []);
+
+ return {
+ File,
+ onOpen
+ };
+};
diff --git a/frontend/providers/workorder/src/hooks/useStore.ts b/frontend/providers/workorder/src/hooks/useStore.ts
new file mode 100644
index 000000000..439fcef7d
--- /dev/null
+++ b/frontend/providers/workorder/src/hooks/useStore.ts
@@ -0,0 +1,17 @@
+import { useState, useEffect } from 'react';
+
+const useStore = (
+ store: (callback: (state: T) => unknown) => unknown,
+ callback: (state: T) => F
+) => {
+ const result = store(callback) as F;
+ const [data, setData] = useState();
+
+ useEffect(() => {
+ setData(result);
+ }, [result]);
+
+ return data;
+};
+
+export default useStore;
diff --git a/frontend/providers/workorder/src/hooks/useToast.ts b/frontend/providers/workorder/src/hooks/useToast.ts
new file mode 100644
index 000000000..f8b1cbe36
--- /dev/null
+++ b/frontend/providers/workorder/src/hooks/useToast.ts
@@ -0,0 +1,12 @@
+import { useToast as uToast, UseToastOptions } from '@chakra-ui/react';
+
+export const useToast = (props?: UseToastOptions) => {
+ const toast = uToast({
+ position: 'top',
+ ...props
+ });
+
+ return {
+ toast
+ };
+};
diff --git a/frontend/providers/workorder/src/pages/404.tsx b/frontend/providers/workorder/src/pages/404.tsx
new file mode 100644
index 000000000..63f53471d
--- /dev/null
+++ b/frontend/providers/workorder/src/pages/404.tsx
@@ -0,0 +1,13 @@
+import { useRouter } from 'next/router';
+import { useEffect } from 'react';
+
+const NonePage = () => {
+ const router = useRouter();
+ useEffect(() => {
+ router.push('/workorders');
+ }, [router]);
+
+ return ;
+};
+
+export default NonePage;
diff --git a/frontend/providers/workorder/src/pages/_app.tsx b/frontend/providers/workorder/src/pages/_app.tsx
new file mode 100644
index 000000000..5f4e1dbdc
--- /dev/null
+++ b/frontend/providers/workorder/src/pages/_app.tsx
@@ -0,0 +1,147 @@
+import { theme } from '@/constants/theme';
+import { useConfirm } from '@/hooks/useConfirm';
+import useEnvStore from '@/store/env';
+import { useGlobalStore } from '@/store/global';
+import useSessionStore from '@/store/session';
+import '@/styles/reset.scss';
+import { getLangStore, setLangStore } from '@/utils/cookieUtils';
+import { ChakraProvider } from '@chakra-ui/react';
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import { throttle } from 'lodash';
+import { appWithTranslation, useTranslation } from 'next-i18next';
+import type { AppProps } from 'next/app';
+import Head from 'next/head';
+import Router, { useRouter } from 'next/router';
+import NProgress from 'nprogress'; //nprogress module
+import 'nprogress/nprogress.css';
+import { useEffect, useState } from 'react';
+import 'react-day-picker/dist/style.css';
+import { EVENT_NAME } from 'sealos-desktop-sdk';
+import { createSealosApp, sealosApp } from 'sealos-desktop-sdk/app';
+
+//Binding events.
+Router.events.on('routeChangeStart', () => NProgress.start());
+Router.events.on('routeChangeComplete', () => NProgress.done());
+Router.events.on('routeChangeError', () => NProgress.done());
+
+// Create a client
+const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: {
+ refetchOnWindowFocus: false,
+ retry: false,
+ cacheTime: 0
+ }
+ }
+});
+
+function App({ Component, pageProps }: AppProps) {
+ const router = useRouter();
+ const { initSystemEnv } = useEnvStore();
+ const { authUser, delAppSession } = useSessionStore();
+ const { i18n } = useTranslation();
+ const { setScreenWidth, setLastRoute } = useGlobalStore();
+ const [refresh, setRefresh] = useState(false);
+
+ const { openConfirm, ConfirmChild } = useConfirm({
+ title: '跳转提示',
+ content: '该应用不允许单独使用,点击确认前往 Sealos Desktop 使用。'
+ });
+
+ useEffect(() => {
+ const response = createSealosApp();
+
+ (async () => {
+ const { domain } = await initSystemEnv();
+ try {
+ const res = await sealosApp.getSession();
+ if (!res?.token) return;
+ authUser(res.token);
+ } catch (err) {
+ if (process.env.NODE_ENV === 'development' && process.env.NEXT_PUBLIC_MOCK_USER) {
+ authUser(process.env.NEXT_PUBLIC_MOCK_USER);
+ } else {
+ delAppSession();
+ openConfirm(() => {
+ window.open(`https://${domain}`, '_self');
+ })();
+ }
+ }
+ })();
+ return response;
+ }, []);
+
+ // add resize event
+ useEffect(() => {
+ const resize = throttle((e: Event) => {
+ const documentWidth = document.documentElement.clientWidth || document.body.clientWidth;
+ setScreenWidth(documentWidth);
+ }, 200);
+ window.addEventListener('resize', resize);
+ const documentWidth = document.documentElement.clientWidth || document.body.clientWidth;
+ setScreenWidth(documentWidth);
+
+ return () => {
+ window.removeEventListener('resize', resize);
+ };
+ }, [setScreenWidth]);
+
+ // init
+ useEffect(() => {
+ const changeI18n = async (data: any) => {
+ const lastLang = getLangStore();
+ const newLang = data.currentLanguage;
+ if (lastLang !== newLang) {
+ i18n?.changeLanguage?.(newLang);
+ setLangStore(newLang);
+ setRefresh((state) => !state);
+ }
+ };
+
+ (async () => {
+ try {
+ const lang = await sealosApp.getLanguage();
+ changeI18n({
+ currentLanguage: lang.lng
+ });
+ } catch (error) {
+ changeI18n({
+ currentLanguage: 'zh'
+ });
+ }
+ })();
+
+ return sealosApp?.addAppEventListen(EVENT_NAME.CHANGE_I18N, changeI18n);
+ }, []);
+
+ // record route
+ useEffect(() => {
+ return () => {
+ setLastRoute(router.asPath);
+ };
+ }, [router.pathname]);
+
+ useEffect(() => {
+ const lang = getLangStore() || 'zh';
+ i18n?.changeLanguage?.(lang);
+ }, [refresh, router.asPath]);
+
+ return (
+ <>
+
+ Sealos Work Order
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+}
+
+export default appWithTranslation(App);
diff --git a/frontend/providers/workorder/src/pages/_document.tsx b/frontend/providers/workorder/src/pages/_document.tsx
new file mode 100644
index 000000000..5cb7e9e66
--- /dev/null
+++ b/frontend/providers/workorder/src/pages/_document.tsx
@@ -0,0 +1,21 @@
+import { Head, Html, Main, NextScript } from 'next/document';
+
+export default function Document() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/frontend/providers/workorder/src/pages/api/ai/fastgpt.ts b/frontend/providers/workorder/src/pages/api/ai/fastgpt.ts
new file mode 100644
index 000000000..913a6581a
--- /dev/null
+++ b/frontend/providers/workorder/src/pages/api/ai/fastgpt.ts
@@ -0,0 +1,87 @@
+import { verifyAccessToken } from '@/services/backend/auth';
+import { jsonRes } from '@/services/backend/response';
+import { getOrderByOrderIdAndUserId } from '@/services/db/workorder';
+import type { NextApiRequest, NextApiResponse } from 'next';
+const fastgpt_url = process.env.FASTGPT_API_URL;
+const fastgpt_key = process.env.FASTGPT_API_KEY;
+const fastgpt_limit = parseInt(process.env.FASTGPT_API_LIMIT || '50');
+
+export default async function handler(req: NextApiRequest, res: NextApiResponse) {
+ try {
+ if (!fastgpt_url || !fastgpt_key) {
+ return jsonRes(res, { code: 500, data: 'miss fastgppt key' });
+ }
+ const payload = await verifyAccessToken(req);
+ if (!payload) {
+ return jsonRes(res, {
+ code: 401,
+ message: "'token is invaild'"
+ });
+ }
+ const { orderId } = req.body as { orderId: string };
+ const result = await getOrderByOrderIdAndUserId({
+ orderId,
+ userId: payload.userId
+ });
+ if (!result || result?.manualHandling.isManuallyHandled) return;
+
+ const robotMessages =
+ result?.dialogs?.filter((dialog) => !dialog.isAdmin && dialog.userId === 'robot') || [];
+
+ const userMessages = result?.dialogs
+ ?.filter((dialog) => !dialog.isAdmin && dialog.userId === payload.userId)
+ .map((item) => {
+ return {
+ content: item.content,
+ role: 'user'
+ };
+ });
+
+ const data = {
+ chatId: orderId,
+ stream: true,
+ detail: false,
+ messages: userMessages
+ };
+
+ res.setHeader('Access-Control-Allow-Origin', '*');
+ res.setHeader('Content-Type', 'text/event-stream;charset=utf-8');
+ res.setHeader('Cache-Control', 'no-cache, no-transform');
+ res.setHeader('X-Accel-Buffering', 'no');
+
+ if (robotMessages.length > fastgpt_limit) {
+ const message = 'If the call limit is exceeded, please transfer it to manual processing.';
+ const eventStream = `event: [LIMIT]\ndata: ${message}\n\n`;
+ res.setHeader('Content-Length', Buffer.byteLength(eventStream, 'utf-8'));
+ res.write(eventStream);
+ res.end();
+ return;
+ }
+
+ const response = await fetch(fastgpt_url, {
+ method: 'POST',
+ headers: {
+ Authorization: `Bearer ${fastgpt_key}`,
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify(data)
+ });
+
+ const reader = response.body?.getReader();
+ if (!reader) return res.end();
+
+ req.socket.on('close', () => {
+ res.end();
+ });
+
+ while (true) {
+ const { value, done } = await reader.read();
+ if (done) {
+ res.end();
+ }
+ res.write(value);
+ }
+ } catch (error) {
+ res.end();
+ }
+}
diff --git a/frontend/providers/workorder/src/pages/api/auth/desktop.ts b/frontend/providers/workorder/src/pages/api/auth/desktop.ts
new file mode 100644
index 000000000..e9e57f514
--- /dev/null
+++ b/frontend/providers/workorder/src/pages/api/auth/desktop.ts
@@ -0,0 +1,66 @@
+import { generateAccessToken, verifyDesktopToken } from '@/services/backend/auth';
+import { jsonRes } from '@/services/backend/response';
+import { createUser, getUserById, updateUser } from '@/services/db/user';
+import { AppSession } from '@/types/user';
+import { NextApiRequest, NextApiResponse } from 'next';
+import { omit } from 'lodash';
+
+export default async function handler(req: NextApiRequest, res: NextApiResponse) {
+ try {
+ const { token } = req.body;
+ const payload = await verifyDesktopToken(token);
+
+ if (!payload) {
+ return jsonRes(res, {
+ code: 401,
+ message: 'verify desktop token error'
+ });
+ }
+ const newPayload = omit(payload, ['iat', 'exp']);
+
+ const existingUser = await getUserById(newPayload.userId);
+
+ if (existingUser) {
+ const temp = {
+ workspaceId: payload.workspaceId,
+ workspaceUid: payload.workspaceUid,
+ regionUid: payload.regionUid,
+ userCrUid: payload.userCrUid,
+ userCrName: payload.userCrName
+ };
+ await updateUser(payload.userId, temp);
+
+ const token = generateAccessToken(existingUser);
+ return jsonRes(res, {
+ code: 200,
+ data: {
+ token: token,
+ user: { ...existingUser, ...temp }
+ }
+ });
+ }
+
+ await createUser({
+ ...newPayload,
+ isAdmin: false
+ });
+
+ const accessToken = generateAccessToken({
+ ...newPayload,
+ isAdmin: false
+ });
+
+ jsonRes(res, {
+ code: 200,
+ data: {
+ token: accessToken,
+ user: {
+ ...newPayload,
+ isAdmin: false
+ }
+ }
+ });
+ } catch (error) {
+ jsonRes(res, { code: 500, error: error });
+ }
+}
diff --git a/frontend/providers/workorder/src/pages/api/auth/findById.ts b/frontend/providers/workorder/src/pages/api/auth/findById.ts
new file mode 100644
index 000000000..16d91582d
--- /dev/null
+++ b/frontend/providers/workorder/src/pages/api/auth/findById.ts
@@ -0,0 +1,42 @@
+import { verifyAccessToken } from '@/services/backend/auth';
+import { jsonRes } from '@/services/backend/response';
+import { getRegionById } from '@/services/db/region';
+import { getOrderByOrderIdAndUserId } from '@/services/db/workorder';
+import { NextApiRequest, NextApiResponse } from 'next';
+
+export default async function handler(req: NextApiRequest, res: NextApiResponse) {
+ try {
+ const payload = await verifyAccessToken(req);
+ if (!payload) {
+ return jsonRes(res, {
+ code: 401,
+ message: "'token is invaild'"
+ });
+ }
+ const { orderId } = req.query as {
+ orderId: string;
+ };
+
+ if (!payload.isAdmin) {
+ return jsonRes(res, {
+ code: 401,
+ message: 'unauthorized'
+ });
+ }
+
+ const result = await getOrderByOrderIdAndUserId({
+ orderId,
+ userId: payload.userId
+ });
+ const regionInfo = await getRegionById(result?.userInfo?.regionUid || '');
+
+ jsonRes(res, {
+ data: {
+ user: result?.userInfo,
+ regionInfo: regionInfo
+ }
+ });
+ } catch (error) {
+ jsonRes(res, { code: 500, data: error });
+ }
+}
diff --git a/frontend/providers/workorder/src/pages/api/minio/delete.ts b/frontend/providers/workorder/src/pages/api/minio/delete.ts
new file mode 100644
index 000000000..638f54bc4
--- /dev/null
+++ b/frontend/providers/workorder/src/pages/api/minio/delete.ts
@@ -0,0 +1,43 @@
+import { jsonRes } from '@/services/backend/response';
+import { NextApiRequest, NextApiResponse } from 'next';
+import { BucketName, minioClient } from './upload';
+import { verifyAccessToken } from '@/services/backend/auth';
+
+export default async function handler(req: NextApiRequest, res: NextApiResponse) {
+ try {
+ if (!process.env?.MINIO_URL || !process.env?.MINIO_SECRET_KEY) {
+ return jsonRes(res, {
+ code: 500,
+ data: 'Missing minio service'
+ });
+ }
+ const payload = await verifyAccessToken(req);
+ if (!payload) {
+ return jsonRes(res, {
+ code: 401,
+ message: "'token is invaild'"
+ });
+ }
+
+ const { fileName } = req.query as {
+ fileName: string;
+ };
+
+ await minioClient.removeObject(BucketName, fileName);
+
+ jsonRes(res, {
+ data: 'File deleted successfully'
+ });
+ } catch (error) {
+ jsonRes(res, {
+ code: 500,
+ error
+ });
+ }
+}
+
+export const config = {
+ api: {
+ bodyParser: false
+ }
+};
diff --git a/frontend/providers/workorder/src/pages/api/minio/getUrl.ts b/frontend/providers/workorder/src/pages/api/minio/getUrl.ts
new file mode 100644
index 000000000..e49afbd0a
--- /dev/null
+++ b/frontend/providers/workorder/src/pages/api/minio/getUrl.ts
@@ -0,0 +1,43 @@
+import { jsonRes } from '@/services/backend/response';
+import { NextApiRequest, NextApiResponse } from 'next';
+import { BucketName, minioClient } from './upload';
+import { verifyAccessToken } from '@/services/backend/auth';
+
+export default async function handler(req: NextApiRequest, res: NextApiResponse) {
+ try {
+ if (!process.env?.MINIO_URL || !process.env?.MINIO_SECRET_KEY) {
+ return jsonRes(res, {
+ code: 500,
+ data: 'Missing minio service'
+ });
+ }
+ const payload = await verifyAccessToken(req);
+ if (!payload) {
+ return jsonRes(res, {
+ code: 401,
+ message: "'token is invaild'"
+ });
+ }
+
+ const { fileName } = req.query as {
+ fileName: string;
+ };
+
+ const url = await minioClient.presignedGetObject(BucketName, fileName);
+
+ jsonRes(res, {
+ data: url
+ });
+ } catch (error) {
+ jsonRes(res, {
+ code: 500,
+ error
+ });
+ }
+}
+
+export const config = {
+ api: {
+ bodyParser: false
+ }
+};
diff --git a/frontend/providers/workorder/src/pages/api/minio/upload.ts b/frontend/providers/workorder/src/pages/api/minio/upload.ts
new file mode 100644
index 000000000..2bd70819b
--- /dev/null
+++ b/frontend/providers/workorder/src/pages/api/minio/upload.ts
@@ -0,0 +1,127 @@
+import { verifyAccessToken } from '@/services/backend/auth';
+import { jsonRes } from '@/services/backend/response';
+import * as Minio from 'minio';
+import multer from 'multer';
+import { customAlphabet } from 'nanoid';
+import { NextApiRequest, NextApiResponse } from 'next';
+import path from 'path';
+const nanoid = customAlphabet('1234567890abcdef', 12);
+
+type FileType = {
+ fieldname: string;
+ originalname: string;
+ encoding: string;
+ mimetype: string;
+ filename: string;
+ path: string;
+ size: number;
+};
+
+/**
+ * Creates the multer uploader
+ */
+const maxSize = 5 * 1024 * 1024 * 1024;
+
+class UploadModel {
+ uploader = multer({
+ limits: {
+ fieldSize: maxSize
+ },
+ preservePath: true,
+ storage: multer.diskStorage({
+ filename: (_req, file, cb) => {
+ const { ext } = path.parse(decodeURIComponent(file.originalname));
+ cb(null, nanoid() + ext);
+ }
+ })
+ }).any();
+
+ async doUpload(req: NextApiRequest, res: NextApiResponse) {
+ return new Promise<{
+ files: FileType[];
+ overwrite: string;
+ }>((resolve, reject) => {
+ // @ts-ignore
+ this.uploader(req, res, (error) => {
+ if (error) {
+ console.log(error, 'multer err');
+ return reject(error);
+ }
+ resolve({
+ ...req.body,
+ files:
+ // @ts-ignore
+ req.files?.map((file) => ({
+ ...file,
+ originalname: decodeURIComponent(file.originalname)
+ })) || []
+ });
+ });
+ });
+ }
+}
+
+const upload = new UploadModel();
+
+export const minioClient = new Minio.Client({
+ endPoint: process.env?.MINIO_URL || 'minioapi.dev.sealos.top',
+ port: Number(process.env?.MINIO_PORT) || 443,
+ accessKey: process.env?.MINIO_ACCESS_KEY || 'database',
+ secretKey: process.env?.MINIO_SECRET_KEY || 'database'
+});
+
+export const BucketName = process.env?.MINIO_BUCKET_NAME || 'database-test';
+
+export default async function handler(req: any, res: NextApiResponse) {
+ try {
+ if (!process.env?.MINIO_URL || !process.env?.MINIO_SECRET_KEY) {
+ return jsonRes(res, {
+ code: 500,
+ data: 'Missing minio service'
+ });
+ }
+ const payload = await verifyAccessToken(req);
+ if (!payload) {
+ return jsonRes(res, {
+ code: 401,
+ message: "'token is invaild'"
+ });
+ }
+ const { files, overwrite = 'true' } = await upload.doUpload(req, res);
+
+ const startTime = performance.now();
+ const upLoadResults = await Promise.all(
+ files.map(async (file) => {
+ let fileName = JSON.parse(overwrite)
+ ? `${payload.userId}-${Date.now()}-${file.filename}`
+ : file.originalname;
+ const originalFileName = file.originalname;
+
+ await minioClient.fPutObject(BucketName, fileName, file.path);
+ return {
+ originalName: originalFileName,
+ fileName: fileName
+ };
+ })
+ );
+ const endTime = performance.now();
+ const duration = endTime - startTime;
+
+ console.log(upLoadResults, `time:${duration} ms`);
+
+ jsonRes(res, {
+ data: upLoadResults
+ });
+ } catch (error) {
+ jsonRes(res, {
+ code: 500,
+ error
+ });
+ }
+}
+
+export const config = {
+ api: {
+ bodyParser: false
+ }
+};
diff --git a/frontend/providers/workorder/src/pages/api/platform/feishu.ts b/frontend/providers/workorder/src/pages/api/platform/feishu.ts
new file mode 100644
index 000000000..91a23c28b
--- /dev/null
+++ b/frontend/providers/workorder/src/pages/api/platform/feishu.ts
@@ -0,0 +1,107 @@
+import { verifyAccessToken } from '@/services/backend/auth';
+import { jsonRes } from '@/services/backend/response';
+import { updateOrder } from '@/services/db/workorder';
+import { ApiResp } from '@/services/kubernet';
+import { WorkOrderType } from '@/types/workorder';
+import type { NextApiRequest, NextApiResponse } from 'next';
+
+export type FeishuNotificationParams = {
+ type: WorkOrderType;
+ description: string;
+ orderId: string;
+ switchToManual?: boolean;
+};
+
+export default async function handler(req: NextApiRequest, res: NextApiResponse) {
+ try {
+ const {
+ type,
+ description,
+ orderId,
+ switchToManual = false
+ } = req.body as FeishuNotificationParams;
+
+ const payload = await verifyAccessToken(req);
+ if (!payload) {
+ return jsonRes(res, {
+ code: 401,
+ message: "'token is invaild'"
+ });
+ }
+ const feishuUrl = process.env.ADMIN_FEISHU_URL;
+ const feishuCallBackUrl = process.env.ADMIN_FEISHU_CALLBACK_URL;
+ const title = switchToManual ? `工单:${orderId},请求人工处理` : '有新的工单,请立即查看';
+
+ if (switchToManual) {
+ await updateOrder({
+ orderId,
+ userId: payload.userId,
+ updates: {
+ manualHandling: { isManuallyHandled: true }
+ }
+ });
+ }
+
+ const form = {
+ msg_type: 'interactive',
+ card: {
+ elements: [
+ {
+ tag: 'markdown',
+ content: `**用户ID:** ${payload.userId}\n**可用区ID:** ${payload.regionUid}\n所属分类: ${type}\n描述信息: ${description}`
+ },
+ {
+ tag: 'action',
+ actions: [
+ {
+ tag: 'button',
+ text: {
+ tag: 'plain_text',
+ content: '查看详情'
+ },
+ type: 'primary',
+ multi_url: {
+ url: feishuCallBackUrl + `?orderId=${orderId}`,
+ android_url: '',
+ ios_url: '',
+ pc_url: ''
+ }
+ }
+ ]
+ }
+ ],
+ header: {
+ template: 'blue',
+ title: {
+ content: title,
+ tag: 'plain_text'
+ }
+ }
+ }
+ };
+
+ if (!feishuUrl) {
+ return jsonRes(res, {
+ code: 500,
+ message: 'Missing Feishu API'
+ });
+ }
+ const data = await fetch(feishuUrl, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify(form)
+ });
+ const result = await data.json();
+ jsonRes(res, {
+ data: result
+ });
+ } catch (error) {
+ console.log(error);
+ jsonRes(res, {
+ code: 500,
+ error
+ });
+ }
+}
diff --git a/frontend/providers/workorder/src/pages/api/platform/getEnv.ts b/frontend/providers/workorder/src/pages/api/platform/getEnv.ts
new file mode 100644
index 000000000..24dee8c5e
--- /dev/null
+++ b/frontend/providers/workorder/src/pages/api/platform/getEnv.ts
@@ -0,0 +1,15 @@
+import { jsonRes } from '@/services/backend/response';
+import { ApiResp } from '@/services/kubernet';
+import type { NextApiRequest, NextApiResponse } from 'next';
+
+export type SystemEnvResponse = {
+ domain?: string;
+};
+
+export default async function handler(req: NextApiRequest, res: NextApiResponse) {
+ jsonRes(res, {
+ data: {
+ domain: process.env.SEALOS_DOMAIN || 'cloud.sealos.io'
+ }
+ });
+}
diff --git a/frontend/providers/workorder/src/pages/api/workorder/create.ts b/frontend/providers/workorder/src/pages/api/workorder/create.ts
new file mode 100644
index 000000000..c4b565991
--- /dev/null
+++ b/frontend/providers/workorder/src/pages/api/workorder/create.ts
@@ -0,0 +1,52 @@
+import { verifyAccessToken } from '@/services/backend/auth';
+import { jsonRes } from '@/services/backend/response';
+import { createOrder } from '@/services/db/workorder';
+import { WorkOrderDB, WorkOrderStatus, WorkOrderType } from '@/types/workorder';
+import { customAlphabet } from 'nanoid';
+import { NextApiRequest, NextApiResponse } from 'next';
+const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz', 12);
+
+export type CreateWorkOrderParams = {
+ type: WorkOrderType;
+ description: string;
+ appendix?: string[];
+};
+
+export default async function handler(req: NextApiRequest, res: NextApiResponse) {
+ try {
+ const { type, description, appendix } = req.body as CreateWorkOrderParams;
+ const payload = await verifyAccessToken(req);
+ if (!payload) {
+ return jsonRes(res, {
+ code: 401,
+ message: "'token is invaild'"
+ });
+ }
+
+ const workorder: WorkOrderDB = {
+ userId: payload.userId,
+ orderId: nanoid(),
+ type,
+ updateTime: new Date(),
+ createTime: new Date(),
+ description,
+ status: WorkOrderStatus.Pending,
+ appendix,
+ manualHandling: {
+ isManuallyHandled: false
+ },
+ userInfo: payload
+ };
+
+ await createOrder({ order: workorder });
+
+ jsonRes(res, {
+ data: {
+ orderId: workorder.orderId
+ }
+ });
+ } catch (error) {
+ console.log(error);
+ jsonRes(res, { code: 500, data: error });
+ }
+}
diff --git a/frontend/providers/workorder/src/pages/api/workorder/delete.ts b/frontend/providers/workorder/src/pages/api/workorder/delete.ts
new file mode 100644
index 000000000..25d37d8f7
--- /dev/null
+++ b/frontend/providers/workorder/src/pages/api/workorder/delete.ts
@@ -0,0 +1,31 @@
+import { verifyAccessToken } from '@/services/backend/auth';
+import { jsonRes } from '@/services/backend/response';
+import { deleteOrder } from '@/services/db/workorder';
+import { NextApiRequest, NextApiResponse } from 'next';
+
+export default async function handler(req: NextApiRequest, res: NextApiResponse) {
+ try {
+ const payload = await verifyAccessToken(req);
+ if (!payload) {
+ return jsonRes(res, {
+ code: 401,
+ message: "'token is invaild'"
+ });
+ }
+
+ const { orderId } = req.query as {
+ orderId: string;
+ };
+
+ const result = await deleteOrder({
+ orderId: orderId,
+ userId: payload.userId
+ });
+
+ jsonRes(res, {
+ data: result
+ });
+ } catch (error) {
+ jsonRes(res, { code: 500, data: error });
+ }
+}
diff --git a/frontend/providers/workorder/src/pages/api/workorder/getByID.ts b/frontend/providers/workorder/src/pages/api/workorder/getByID.ts
new file mode 100644
index 000000000..7422acc51
--- /dev/null
+++ b/frontend/providers/workorder/src/pages/api/workorder/getByID.ts
@@ -0,0 +1,36 @@
+import { verifyAccessToken } from '@/services/backend/auth';
+import { jsonRes } from '@/services/backend/response';
+import { getOrderByOrderIdAndUserId } from '@/services/db/workorder';
+import { NextApiRequest, NextApiResponse } from 'next';
+
+export default async function handler(req: NextApiRequest, res: NextApiResponse) {
+ try {
+ const payload = await verifyAccessToken(req);
+ if (!payload) {
+ return jsonRes(res, {
+ code: 401,
+ message: "'token is invaild'"
+ });
+ }
+ const { orderId } = req.query as {
+ orderId: string;
+ };
+
+ const result = await getOrderByOrderIdAndUserId({
+ orderId,
+ userId: payload.userId
+ });
+
+ if (!result) {
+ return jsonRes(res, {
+ code: 404,
+ message: 'Not Found'
+ });
+ }
+ jsonRes(res, {
+ data: result
+ });
+ } catch (error) {
+ jsonRes(res, { code: 500, data: error });
+ }
+}
diff --git a/frontend/providers/workorder/src/pages/api/workorder/listByUser.ts b/frontend/providers/workorder/src/pages/api/workorder/listByUser.ts
new file mode 100644
index 000000000..9fbb7fdd4
--- /dev/null
+++ b/frontend/providers/workorder/src/pages/api/workorder/listByUser.ts
@@ -0,0 +1,50 @@
+import { verifyAccessToken } from '@/services/backend/auth';
+import { jsonRes } from '@/services/backend/response';
+import { getAllOrdersByUserId } from '@/services/db/workorder';
+import { WorkOrderStatus, WorkOrderType } from '@/types/workorder';
+import { NextApiRequest, NextApiResponse } from 'next';
+
+export default async function handler(req: NextApiRequest, res: NextApiResponse) {
+ try {
+ const {
+ page = 1,
+ pageSize = 10,
+ orderStatus,
+ orderType,
+ startTime,
+ endTime
+ } = req.body as {
+ page: number;
+ pageSize: number;
+ orderType?: WorkOrderType;
+ orderStatus?: WorkOrderStatus;
+ startTime?: Date;
+ endTime?: Date;
+ };
+
+ const payload = await verifyAccessToken(req);
+ if (!payload) {
+ return jsonRes(res, {
+ code: 401,
+ message: "'token is invaild'"
+ });
+ }
+
+ let result = await getAllOrdersByUserId({
+ userId: payload.userId,
+ page: page,
+ pageSize: pageSize,
+ orderStatus,
+ orderType,
+ startTime,
+ endTime
+ });
+
+ jsonRes(res, {
+ data: result
+ });
+ } catch (error) {
+ console.log(error);
+ jsonRes(res, { code: 500, error: error });
+ }
+}
diff --git a/frontend/providers/workorder/src/pages/api/workorder/update.ts b/frontend/providers/workorder/src/pages/api/workorder/update.ts
new file mode 100644
index 000000000..08e233414
--- /dev/null
+++ b/frontend/providers/workorder/src/pages/api/workorder/update.ts
@@ -0,0 +1,33 @@
+import { verifyAccessToken } from '@/services/backend/auth';
+import { jsonRes } from '@/services/backend/response';
+import { updateOrder } from '@/services/db/workorder';
+import { WorkOrderDB } from '@/types/workorder';
+import { NextApiRequest, NextApiResponse } from 'next';
+
+export default async function handler(req: NextApiRequest, res: NextApiResponse) {
+ try {
+ const payload = await verifyAccessToken(req);
+ if (!payload) {
+ return jsonRes(res, {
+ code: 401,
+ message: "'token is invaild'"
+ });
+ }
+ const { orderId, updates } = req.body as {
+ updates: Partial;
+ orderId: string;
+ };
+
+ const result = await updateOrder({
+ orderId,
+ userId: payload.userId,
+ updates
+ });
+
+ jsonRes(res, {
+ data: result
+ });
+ } catch (error) {
+ jsonRes(res, { code: 500, data: error });
+ }
+}
diff --git a/frontend/providers/workorder/src/pages/api/workorder/updateDialog.ts b/frontend/providers/workorder/src/pages/api/workorder/updateDialog.ts
new file mode 100644
index 000000000..a44906c19
--- /dev/null
+++ b/frontend/providers/workorder/src/pages/api/workorder/updateDialog.ts
@@ -0,0 +1,56 @@
+import { verifyAccessToken } from '@/services/backend/auth';
+import { jsonRes } from '@/services/backend/response';
+import { addDialogToOrder, updateOrder } from '@/services/db/workorder';
+import { WorkOrderStatus } from '@/types/workorder';
+import { NextApiRequest, NextApiResponse } from 'next';
+
+export default async function handler(req: NextApiRequest, res: NextApiResponse) {
+ try {
+ const payload = await verifyAccessToken(req);
+ if (!payload) {
+ return jsonRes(res, {
+ code: 401,
+ message: "'token is invaild'"
+ });
+ }
+ const {
+ orderId,
+ content,
+ isRobot = false
+ } = req.body as {
+ orderId: string;
+ content: string;
+ isRobot: boolean;
+ };
+
+ // If the admin replies, it is being processed.
+ if (payload.isAdmin) {
+ await updateOrder({
+ orderId,
+ userId: payload.userId,
+ updates: {
+ status: WorkOrderStatus.Processing,
+ manualHandling: { isManuallyHandled: true, handlingTime: new Date() }
+ }
+ });
+ }
+
+ const result = await addDialogToOrder({
+ orderId,
+ userId: payload.userId,
+ dialog: {
+ userId: isRobot ? 'robot' : payload.userId,
+ time: new Date(),
+ content: content,
+ isAdmin: payload.isAdmin,
+ isAIBot: isRobot
+ }
+ });
+
+ jsonRes(res, {
+ data: result
+ });
+ } catch (error) {
+ jsonRes(res, { code: 500, data: error });
+ }
+}
diff --git a/frontend/providers/workorder/src/pages/index.tsx b/frontend/providers/workorder/src/pages/index.tsx
new file mode 100644
index 000000000..4531d85e3
--- /dev/null
+++ b/frontend/providers/workorder/src/pages/index.tsx
@@ -0,0 +1,22 @@
+import { useRouter } from 'next/router';
+import { useEffect } from 'react';
+
+export default function Index() {
+ const router = useRouter();
+
+ useEffect(() => {
+ if (router?.query?.orderId) {
+ const orderId = router?.query?.orderId;
+ router.push({
+ pathname: '/workorder/detail',
+ query: {
+ orderId: orderId
+ }
+ });
+ } else {
+ router.push('/workorders');
+ }
+ }, [router]);
+
+ return ;
+}
diff --git a/frontend/providers/workorder/src/pages/workorder/create/components/ErrorModal.tsx b/frontend/providers/workorder/src/pages/workorder/create/components/ErrorModal.tsx
new file mode 100644
index 000000000..707bf7ced
--- /dev/null
+++ b/frontend/providers/workorder/src/pages/workorder/create/components/ErrorModal.tsx
@@ -0,0 +1,41 @@
+import React from 'react';
+import {
+ Modal,
+ ModalOverlay,
+ ModalContent,
+ ModalHeader,
+ ModalBody,
+ ModalCloseButton,
+ Box
+} from '@chakra-ui/react';
+import MyIcon from '@/components/Icon';
+
+const ErrorModal = ({
+ title,
+ content,
+ onClose
+}: {
+ title: string;
+ content: string;
+ onClose: () => void;
+}) => {
+ return (
+
+
+
+
+
+
+ {title}
+
+
+
+
+ {content}
+
+
+
+ );
+};
+
+export default ErrorModal;
diff --git a/frontend/providers/workorder/src/pages/workorder/create/components/Header.tsx b/frontend/providers/workorder/src/pages/workorder/create/components/Header.tsx
new file mode 100644
index 000000000..b46528212
--- /dev/null
+++ b/frontend/providers/workorder/src/pages/workorder/create/components/Header.tsx
@@ -0,0 +1,44 @@
+import MyIcon from '@/components/Icon';
+import { useGlobalStore } from '@/store/global';
+import { Box, Button, Flex } from '@chakra-ui/react';
+import { useTranslation } from 'next-i18next';
+import { useRouter } from 'next/router';
+
+const Header = ({
+ title,
+ applyCb,
+ applyBtnText
+}: {
+ title: string;
+ applyCb: () => void;
+ applyBtnText: string;
+}) => {
+ const { t } = useTranslation();
+ const router = useRouter();
+ const { lastRoute } = useGlobalStore();
+
+ return (
+
+ router.replace(lastRoute)}>
+
+
+ {t(title)}
+
+
+
+
+
+ );
+};
+
+export default Header;
diff --git a/frontend/providers/workorder/src/pages/workorder/create/index.tsx b/frontend/providers/workorder/src/pages/workorder/create/index.tsx
new file mode 100644
index 000000000..283d1aaf6
--- /dev/null
+++ b/frontend/providers/workorder/src/pages/workorder/create/index.tsx
@@ -0,0 +1,214 @@
+import { createWorkOrder, updateWorkOrderDialogById } from '@/api/workorder';
+import FileSelect, { FileItemType } from '@/components/FileSelect';
+import MyIcon from '@/components/Icon';
+import MySelect from '@/components/Select';
+import { OrderTypeList } from '@/constants/workorder';
+import { useConfirm } from '@/hooks/useConfirm';
+import { useLoading } from '@/hooks/useLoading';
+import { useToast } from '@/hooks/useToast';
+import { useGlobalStore } from '@/store/global';
+import { WorkOrderEditForm, WorkOrderType } from '@/types/workorder';
+import { serviceSideProps } from '@/utils/i18n';
+import { Box, BoxProps, Flex, Icon, Text, Textarea } from '@chakra-ui/react';
+import { useTranslation } from 'next-i18next';
+import { useRouter } from 'next/router';
+import { useCallback, useState } from 'react';
+import { useForm } from 'react-hook-form';
+import ErrorModal from './components/ErrorModal';
+import Header from './components/Header';
+import { FeishuNotification, uploadFile } from '@/api/platform';
+
+export default function EditOrder() {
+ const [errorMessage, setErrorMessage] = useState('');
+ const [files, setFiles] = useState([]);
+ const { t } = useTranslation();
+ const { toast } = useToast();
+ const { Loading, setIsLoading } = useLoading();
+ const router = useRouter();
+ const { screenWidth, lastRoute } = useGlobalStore();
+ const [forceUpdate, setForceUpdate] = useState(false);
+ const [fileProgressText, setFileProgressText] = useState('');
+
+ const { openConfirm, ConfirmChild } = useConfirm({
+ content: t('Are you sure you want to submit this work order')
+ });
+
+ // form
+ const formHook = useForm({
+ defaultValues: {
+ type: WorkOrderType.AppLaunchpad,
+ description: ''
+ }
+ });
+
+ // watch form change, compute new yaml
+ formHook.watch((data) => {
+ setForceUpdate(!forceUpdate);
+ });
+
+ const submitSuccess = async (data: WorkOrderEditForm) => {
+ setIsLoading(true);
+ try {
+ const form = new FormData();
+ files.forEach((item) => {
+ form.append('file', item.file, encodeURIComponent(item.filename));
+ });
+ form.append('overwrite', 'false');
+ const result = await uploadFile(form);
+ const res = await createWorkOrder({
+ type: data.type,
+ description: data.description,
+ appendix: result.map((i) => i.fileName)
+ });
+ await updateWorkOrderDialogById({
+ orderId: res.orderId,
+ content: data.description
+ });
+ await FeishuNotification({
+ type: data.type,
+ description: data.description,
+ orderId: res.orderId
+ });
+ toast({
+ status: 'success',
+ title: 'success'
+ });
+ router.push(`/workorder/detail?orderId=${res.orderId}`);
+ } catch (error) {
+ console.error(error);
+ setErrorMessage(JSON.stringify(error));
+ }
+ setIsLoading(false);
+ };
+
+ const submitError = useCallback(() => {
+ // deep search message
+ const deepSearch = (obj: any): string => {
+ if (!obj) return t('Submit Error');
+ if (!!obj.message) {
+ return obj.message;
+ }
+ return deepSearch(Object.values(obj)[0]);
+ };
+
+ toast({
+ title: deepSearch(formHook.formState.errors),
+ status: 'error',
+ position: 'top',
+ duration: 3000,
+ isClosable: true
+ });
+ }, [formHook.formState.errors, t, toast]);
+
+ const Label = ({
+ children,
+ w = 'auto',
+ LabelStyle
+ }: {
+ children: string;
+ w?: number | 'auto';
+ LabelStyle?: BoxProps;
+ }) => (
+
+ {children}
+
+ );
+
+ return (
+
+
+ formHook.handleSubmit((data) => openConfirm(() => submitSuccess(data))(), submitError)()
+ }
+ applyBtnText="Submit Order"
+ />
+
+
+
+
+
+ {t('Problem Description')}
+
+
+
+
+
+ {
+ formHook.setValue('type', val);
+ }}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {!!errorMessage && (
+ setErrorMessage('')} />
+ )}
+
+ );
+}
+
+export async function getServerSideProps(context: any) {
+ return {
+ props: { ...(await serviceSideProps(context)) }
+ };
+}
diff --git a/frontend/providers/workorder/src/pages/workorder/detail/components/AppBaseInfo.tsx b/frontend/providers/workorder/src/pages/workorder/detail/components/AppBaseInfo.tsx
new file mode 100644
index 000000000..fa1be8575
--- /dev/null
+++ b/frontend/providers/workorder/src/pages/workorder/detail/components/AppBaseInfo.tsx
@@ -0,0 +1,178 @@
+import { getFileUrl } from '@/api/platform';
+import { findUserById } from '@/api/user';
+import { FileImgs } from '@/components/FileSelect';
+import MyIcon from '@/components/Icon';
+import useSessionStore from '@/store/session';
+import { WorkOrderDB } from '@/types/workorder';
+import { useCopyData } from '@/utils/tools';
+import { Box, Flex, Icon, Tag, Text } from '@chakra-ui/react';
+import { useQuery } from '@tanstack/react-query';
+import { useTranslation } from 'next-i18next';
+import { useMemo } from 'react';
+
+const AppBaseInfo = ({ app }: { app: WorkOrderDB }) => {
+ const { t } = useTranslation();
+ const { copyData } = useCopyData();
+ const { session } = useSessionStore();
+
+ const appendixs = useMemo(() => {
+ if (!app?.appendix) return [];
+ return app?.appendix.map((fileName) => {
+ let temp = FileImgs.find((i) => i.reg.exec(fileName));
+ return {
+ fileName,
+ fileIcon: temp ? temp.src : FileImgs[0].src
+ };
+ });
+ }, [app?.appendix]);
+
+ const handleFile = async (name: string) => {
+ const fileUrl = await getFileUrl({ fileName: name });
+ window.open(fileUrl);
+ };
+
+ const { data: userInfo } = useQuery(
+ ['findUserById'],
+ () => findUserById({ orderId: app?.orderId || '' }),
+ {
+ enabled: !!app?.orderId && session?.user?.isAdmin
+ }
+ );
+
+ return (
+
+
+
+
+
+
+
+ {t('Question Type')}
+
+
+
+
+ {app?.type}
+
+
+ {appendixs?.length > 0 && (
+
+
+
+ {t('appendix')}
+
+
+ {appendixs?.map((item) => (
+
+ {item.fileIcon}
+ {item.fileName}
+ handleFile(item.fileName)}
+ >
+
+
+
+ ))}
+
+
+ )}
+
+
+
+
+
+
+
+ {t('Description')}
+
+
+ {app?.description}
+
+
+
+ {session?.user?.isAdmin && (
+
+
+
+
+
+
+
+ {t('user info')}(Admin)
+
+
+
+ copyData(userInfo?.user?.userId || '')}>
+ userID: {userInfo?.user?.userId}
+
+ copyData(userInfo?.user?.userCrName || '')}>
+ userCrName: {userInfo?.user?.userCrName}
+
+ copyData(userInfo?.user?.workspaceId || '')}>
+ workspaceId: {userInfo?.user?.workspaceId}
+
+ copyData(userInfo?.regionInfo?.sealosRegionDomain || '')}>
+ region: {userInfo?.regionInfo?.sealosRegionDomain}
+
+
+
+ )}
+
+ );
+};
+
+export default AppBaseInfo;
diff --git a/frontend/providers/workorder/src/pages/workorder/detail/components/AppMainInfo.tsx b/frontend/providers/workorder/src/pages/workorder/detail/components/AppMainInfo.tsx
new file mode 100644
index 000000000..ec604a0f5
--- /dev/null
+++ b/frontend/providers/workorder/src/pages/workorder/detail/components/AppMainInfo.tsx
@@ -0,0 +1,580 @@
+import { FeishuNotification, deleteFileByName, getFileUrl, uploadFile } from '@/api/platform';
+import { updateWorkOrderDialogById } from '@/api/workorder';
+import MyIcon from '@/components/Icon';
+import Markdown from '@/components/Markdown';
+import { useSelectFile } from '@/hooks/useSelectFile';
+import { useToast } from '@/hooks/useToast';
+import useSessionStore from '@/store/session';
+import { WorkOrderDB, WorkOrderDialog } from '@/types/workorder';
+import { isURL } from '@/utils/file';
+import { formatTime } from '@/utils/tools';
+import {
+ Box,
+ Button,
+ Center,
+ Flex,
+ Icon,
+ Image,
+ Spinner,
+ Text,
+ Textarea,
+ keyframes
+} from '@chakra-ui/react';
+import { fetchEventSource } from '@fortaine/fetch-event-source';
+import { throttle } from 'lodash';
+import { useTranslation } from 'next-i18next';
+import { useEffect, useLayoutEffect, useRef, useState } from 'react';
+
+const statusAnimation = keyframes`
+ 0% {
+ opacity: 1;
+ }
+
+ 100% {
+ opacity: 0.11;
+ }
+`;
+
+const CommandTip = () => {
+ const { t } = useTranslation();
+
+ return (
+
+
+
+
+
+ {t('send')}
+ /
+
+
+
+
+
+
+
+
+
+
+ {t('newline')}
+
+ );
+};
+
+const AppMainInfo = ({
+ app,
+ refetchWorkOrder,
+ isManuallyHandled
+}: {
+ app: WorkOrderDB;
+ refetchWorkOrder: () => void;
+ isManuallyHandled: boolean;
+}) => {
+ const textareaMinH = '50px';
+ const { session } = useSessionStore();
+ const { t } = useTranslation();
+ const [text, setText] = useState('');
+ const { toast } = useToast();
+ const [dialogs, setDialogs] = useState(app?.dialogs || []);
+ const [isChatLoading, setIsChatLoading] = useState(false);
+ const messageBoxRef = useRef(null);
+ const TextareaDom = useRef(null);
+ const [uploadedFiles, setUploadedFiles] = useState<
+ {
+ fileName: string;
+ originalName: string;
+ fileUrl: string;
+ }[]
+ >();
+
+ const { File, onOpen } = useSelectFile({
+ fileType: 'image/*',
+ multiple: true
+ });
+
+ const removeFile = async (fileName: string) => {
+ await deleteFileByName({ fileName: fileName });
+ setUploadedFiles(
+ (prevFiles) => prevFiles && prevFiles.filter((file) => file.fileName !== fileName)
+ );
+ };
+
+ const uploadFiles = async (files: File[]) => {
+ const form = new FormData();
+ files.forEach((item) => {
+ form.append('file', item, encodeURIComponent(item.name));
+ });
+ const temp = await uploadFile(form);
+ setUploadedFiles(temp.map((i) => ({ ...i, fileUrl: '' })));
+
+ const result = await Promise.all(
+ temp.map(async (i) => {
+ const fileUrl = await getFileUrl({ fileName: i.fileName });
+ return {
+ fileName: i.fileName,
+ originalName: i.originalName,
+ fileUrl: fileUrl
+ };
+ })
+ );
+ setUploadedFiles(result);
+ };
+
+ const handleSend = async () => {
+ if (!session) return;
+ const promises: Promise[] = [];
+ const temps: WorkOrderDialog[] = [];
+ if (isChatLoading) {
+ toast({
+ status: 'info',
+ title: t('please wait for the end')
+ });
+ return;
+ }
+ setIsChatLoading(true);
+ try {
+ if (uploadedFiles) {
+ uploadedFiles.forEach((i) => {
+ const temp: WorkOrderDialog = {
+ time: new Date(),
+ content: i.fileUrl,
+ userId: session?.user?.userId,
+ isAdmin: session?.user?.isAdmin,
+ isAIBot: false
+ };
+ temps.push(temp);
+ promises.push(
+ updateWorkOrderDialogById({
+ orderId: app.orderId,
+ content: temp.content
+ })
+ );
+ });
+ setUploadedFiles(undefined);
+ }
+ if (text !== '') {
+ const temp: WorkOrderDialog = {
+ time: new Date(),
+ content: text,
+ userId: session?.user?.userId,
+ isAdmin: session?.user?.isAdmin,
+ isAIBot: false
+ };
+ temps.push(temp);
+ promises.push(
+ updateWorkOrderDialogById({
+ orderId: app.orderId,
+ content: temp.content
+ })
+ );
+ setText('');
+ }
+
+ setDialogs((v) => [...v, ...temps]);
+ await Promise.all(promises);
+ if (text !== '' && !session?.user?.isAdmin) {
+ await triggerRobotReply();
+ }
+ } catch (error) {
+ console.log(error);
+ toast({ title: t('network anomaly'), status: 'error' });
+ setDialogs((v) => v.slice(0, v.length - temps.length));
+ setText('');
+ setUploadedFiles(undefined);
+ }
+ setIsChatLoading(false);
+ };
+
+ const triggerRobotReply = async () => {
+ let temp = '';
+ try {
+ if (!app.manualHandling.isManuallyHandled && !session?.user?.isAdmin) {
+ await fetchEventSource('/api/ai/fastgpt', {
+ method: 'POST',
+ headers: {
+ Authorization: useSessionStore.getState()?.session?.token || '',
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({
+ orderId: app.orderId
+ }),
+ openWhenHidden: true,
+ onmessage: async ({ event, data }) => {
+ if (data === '[DONE]') {
+ return;
+ }
+ if (event === '[LIMIT]') {
+ toast({ status: 'error', title: t('Request exceeds limit') });
+ handleTransferToHuman();
+ return;
+ }
+ if (event === 'error') {
+ toast({ status: 'error', title: t('api is error') });
+ return;
+ }
+
+ const json = JSON.parse(data);
+
+ const text = json?.choices?.[0]?.delta?.content || '';
+ temp += text;
+
+ if (temp && temp !== '') {
+ const updatedLastDialog = {
+ time: new Date(),
+ userId: 'robot',
+ isAdmin: false,
+ isAIBot: true,
+ content: temp
+ };
+ setDialogs((prevDialogs) => {
+ const lastDialog = prevDialogs[prevDialogs.length - 1];
+ if (lastDialog && lastDialog.userId === 'robot') {
+ return [...prevDialogs.slice(0, -1), updatedLastDialog];
+ } else {
+ return [...prevDialogs, updatedLastDialog];
+ }
+ });
+ }
+ },
+ onerror(err) {
+ console.log(err);
+ }
+ });
+
+ if (temp !== '') {
+ await updateWorkOrderDialogById({
+ orderId: app.orderId,
+ content: temp,
+ isRobot: true
+ });
+ }
+ }
+ } catch (error) {
+ console.log(error);
+ }
+ };
+
+ const handleTransferToHuman = async () => {
+ try {
+ await FeishuNotification({
+ type: app.type,
+ description: app.description,
+ orderId: app.orderId,
+ switchToManual: true
+ });
+ toast({
+ title: t('Notification SwitchToManual Tips'),
+ status: 'success'
+ });
+ } catch (error) {
+ toast({
+ title: t('network anomaly'),
+ status: 'error'
+ });
+ }
+ refetchWorkOrder();
+ };
+
+ const scrollToBottom = useRef(
+ throttle(() => {
+ const boxElement = messageBoxRef.current;
+ if (boxElement) {
+ requestAnimationFrame(() => {
+ boxElement.scrollTop = boxElement.scrollHeight;
+ });
+ }
+ }, 1000)
+ ).current;
+
+ useLayoutEffect(() => {
+ scrollToBottom();
+ }, [dialogs, scrollToBottom]);
+
+ useEffect(() => {
+ if (app?.dialogs?.length === 1) {
+ triggerRobotReply();
+ }
+ }, []);
+
+ useEffect(() => {
+ if (isManuallyHandled && app?.dialogs) {
+ setDialogs(app?.dialogs);
+ }
+ }, [app?.dialogs, isManuallyHandled]);
+
+ return (
+ <>
+
+ {t('Order Conversation')}
+
+
+ {dialogs &&
+ dialogs?.map((item, index) => {
+ return (
+
+ {index === 0 ||
+ (dialogs?.[index - 1] &&
+ new Date(dialogs[index - 1].time).getTime() <
+ new Date(item.time).getTime() - 5 * 60 * 1000) ? (
+
+ {formatTime(item.time, 'YYYY-MM-DD HH:mm')}
+
+ ) : null}
+
+
+
+
+
+
+
+ {item.isAdmin ? (
+
+ Sealos 支持
+ ID:{item.userId}
+
+ ) : item.isAIBot && isChatLoading ? (
+
+ Robot
+
+ ) : (
+ item.userId
+ )}
+
+
+
+ {isURL(item.content) ? (
+ scrollToBottom()}>
+ ) : (
+
+
+
+ )}
+
+ {index === dialogs?.length - 1 &&
+ !session?.user.isAdmin &&
+ !app?.manualHandling?.isManuallyHandled && (
+
+ )}
+
+
+
+ );
+ })}
+
+
+
+ {uploadedFiles && (
+
+ {uploadedFiles.map((file, index) => (
+
+
+
+
+
+
+
+
+
+
+
+ {file.originalName}
+ removeFile(file.fileName)}
+ >
+ {file.fileUrl ? (
+
+
+
+ ) : (
+
+ )}
+
+
+ ))}
+
+ )}
+
+
+ >
+ );
+};
+
+export default AppMainInfo;
diff --git a/frontend/providers/workorder/src/pages/workorder/detail/components/Header.tsx b/frontend/providers/workorder/src/pages/workorder/detail/components/Header.tsx
new file mode 100644
index 000000000..a835c2fae
--- /dev/null
+++ b/frontend/providers/workorder/src/pages/workorder/detail/components/Header.tsx
@@ -0,0 +1,104 @@
+import { updateWorkOrderById } from '@/api/workorder';
+import AppStatusTag from '@/components/AppStatusTag';
+import MyIcon from '@/components/Icon';
+import { useToast } from '@/hooks/useToast';
+import { useGlobalStore } from '@/store/global';
+import { WorkOrderDB, WorkOrderStatus } from '@/types/workorder';
+import { Box, Button, Flex } from '@chakra-ui/react';
+import { useTranslation } from 'next-i18next';
+import { useRouter } from 'next/router';
+import { Dispatch, useCallback } from 'react';
+
+const Header = ({
+ app,
+ isLargeScreen = true,
+ setShowSlider
+}: {
+ app: WorkOrderDB;
+ isLargeScreen: boolean;
+ setShowSlider: Dispatch;
+}) => {
+ const { t } = useTranslation();
+ const router = useRouter();
+ const { lastRoute } = useGlobalStore();
+ const { toast } = useToast();
+
+ const handleWorkOrder = useCallback(
+ async (id: string, method: 'delete' | 'close') => {
+ try {
+ await updateWorkOrderById({
+ orderId: id,
+ updates:
+ method === 'delete'
+ ? {
+ status: WorkOrderStatus.Deleted
+ }
+ : {
+ status: WorkOrderStatus.Completed
+ }
+ });
+ toast({
+ title: `success`,
+ status: 'success'
+ });
+ router.push('/workorders');
+ } catch (error) {}
+ },
+ [router, toast]
+ );
+
+ return (
+
+ router.replace('/workorders')}>
+
+
+ {t('Order Detail')}
+
+ {app?.status && }
+
+
+ {!isLargeScreen && (
+
+ }
+ variant={'base'}
+ bg={'white'}
+ onClick={() => setShowSlider(true)}
+ >
+ {t('Details')}
+
+
+ )}
+ }
+ variant={'base'}
+ bg={'white'}
+ onClick={() => handleWorkOrder(app.orderId, 'close')}
+ >
+ {t('Close')}
+
+ }
+ variant={'base'}
+ bg={'white'}
+ _hover={{
+ color: '#FF324A'
+ }}
+ onClick={() => handleWorkOrder(app.orderId, 'delete')}
+ >
+ {t('Delete')}
+
+
+ );
+};
+
+export default Header;
diff --git a/frontend/providers/workorder/src/pages/workorder/detail/index.tsx b/frontend/providers/workorder/src/pages/workorder/detail/index.tsx
new file mode 100644
index 000000000..714dcf3ec
--- /dev/null
+++ b/frontend/providers/workorder/src/pages/workorder/detail/index.tsx
@@ -0,0 +1,132 @@
+import { getWorkOrderById } from '@/api/workorder';
+import { useLoading } from '@/hooks/useLoading';
+import { useGlobalStore } from '@/store/global';
+import { serviceSideProps } from '@/utils/i18n';
+import { Box, Flex } from '@chakra-ui/react';
+import { useQuery } from '@tanstack/react-query';
+import { useTranslation } from 'next-i18next';
+import { useMemo, useRef, useState } from 'react';
+import AppBaseInfo from './components/AppBaseInfo';
+import AppMainInfo from './components/AppMainInfo';
+import Header from './components/Header';
+import { WorkOrderDB } from '@/types/workorder';
+import { useRouter } from 'next/router';
+
+export default function OrderDetail({ orderId }: { orderId: string }) {
+ const { t } = useTranslation();
+ const [showSlider, setShowSlider] = useState(false);
+ const { screenWidth } = useGlobalStore();
+ const { Loading } = useLoading();
+ const isLargeScreen = useMemo(() => screenWidth > 1100, [screenWidth]);
+ const router = useRouter();
+ const [isHandled, setIsHandled] = useState(false);
+ const [interval, setInterval] = useState(5 * 1000);
+ const prevDataRef = useRef(null);
+
+ const { data: workOrderDetail, refetch: refetchWorkOrder } = useQuery(
+ ['getWorkOrderById', orderId],
+ () => getWorkOrderById({ orderId }),
+ {
+ refetchInterval: isHandled ? interval : false,
+ onSuccess(data) {
+ setIsHandled(data?.manualHandling?.isManuallyHandled || false);
+ if (isHandled) {
+ const currDialogsLen = data?.dialogs?.length;
+ const prevDialogsLen = prevDataRef.current?.dialogs?.length;
+
+ if (!prevDataRef.current || currDialogsLen !== prevDialogsLen) {
+ setInterval(5 * 1000);
+ } else {
+ const newInterval = Math.min(interval + 5 * 1000, 60 * 1000);
+ setInterval(newInterval);
+ }
+ prevDataRef.current = data;
+ }
+ },
+ onError(err: any) {
+ if (err?.code === 404) {
+ router.push('/');
+ }
+ }
+ }
+ );
+
+ return (
+
+ {workOrderDetail && (
+
+ )}
+
+
+ {workOrderDetail && }
+
+
+ {workOrderDetail && workOrderDetail.dialogs?.length ? (
+
+ ) : (
+
+ )}
+
+
+
+ {!isLargeScreen && showSlider && (
+ setShowSlider(false)}
+ />
+ )}
+
+ );
+}
+
+export async function getServerSideProps(context: any) {
+ const orderId = context.query?.orderId || '';
+
+ return {
+ props: { ...(await serviceSideProps(context)), orderId }
+ };
+}
diff --git a/frontend/providers/workorder/src/pages/workorders/components/List.tsx b/frontend/providers/workorder/src/pages/workorders/components/List.tsx
new file mode 100644
index 000000000..cf0c2abbf
--- /dev/null
+++ b/frontend/providers/workorder/src/pages/workorders/components/List.tsx
@@ -0,0 +1,179 @@
+import { updateWorkOrderById } from '@/api/workorder';
+import AppStatusTag from '@/components/AppStatusTag';
+import MyIcon from '@/components/Icon';
+import MyMenu from '@/components/Menu';
+import MyTable from '@/components/Table';
+import { useToast } from '@/hooks/useToast';
+import { WorkOrderDB, WorkOrderStatus } from '@/types/workorder';
+import { formatTime } from '@/utils/tools';
+import { Box, Button, Flex, FlexProps, MenuButton } from '@chakra-ui/react';
+import { useTranslation } from 'next-i18next';
+import { useRouter } from 'next/router';
+import { useCallback, useMemo } from 'react';
+
+const OrderList = ({
+ apps = [],
+ refetchApps
+}: {
+ apps: WorkOrderDB[];
+ refetchApps: () => void;
+}) => {
+ const router = useRouter();
+ const { t } = useTranslation();
+ const { toast } = useToast();
+
+ const handleWorkOrder = useCallback(
+ async (id: string, method: 'delete' | 'close') => {
+ try {
+ await updateWorkOrderById({
+ orderId: id,
+ updates:
+ method === 'delete'
+ ? {
+ status: WorkOrderStatus.Deleted
+ }
+ : {
+ status: WorkOrderStatus.Completed
+ }
+ });
+ toast({
+ title: `success`,
+ status: 'success'
+ });
+ } catch (error) {}
+ refetchApps();
+ },
+ [refetchApps, toast]
+ );
+
+ const columns = useMemo<
+ {
+ title: string;
+ dataIndex?: keyof WorkOrderDB;
+ key: string;
+ render?: (item: WorkOrderDB) => JSX.Element;
+ ItemStyle?: FlexProps;
+ }[]
+ >(
+ () => [
+ {
+ title: 'Order Name',
+ key: 'name',
+ render: (item: WorkOrderDB) => {
+ return (
+
+ {item.orderId}
+
+ );
+ },
+ ItemStyle: {
+ pl: '28px'
+ }
+ },
+ {
+ title: 'Problem Description',
+ key: 'description',
+ render: (item: WorkOrderDB) => {
+ return (
+
+ {item.description}
+
+ );
+ },
+ ItemStyle: {
+ flexBasis: '180px'
+ }
+ },
+ {
+ title: 'Status',
+ key: 'status',
+ render: (item: WorkOrderDB) => {
+ return ;
+ }
+ },
+ {
+ title: 'Create Time',
+ key: 'createTime',
+ render: (item: WorkOrderDB) => {
+ return (
+
+ {formatTime(item.createTime, 'YYYY-MM-DD HH:mm')}
+
+ );
+ }
+ },
+ {
+ title: 'Question Type',
+ key: 'QuestionType',
+ render: (item: WorkOrderDB) => {
+ return (
+
+ {item.type}
+
+ );
+ }
+ },
+ {
+ title: 'Operation',
+ key: 'control',
+ render: (item: WorkOrderDB) => (
+
+ }
+ px={'8px'}
+ onClick={() => {
+ router.push(`/workorder/detail?orderId=${item.orderId}`);
+ }}
+ >
+ {t('Details')}
+
+
+
+
+ }
+ menuList={[
+ {
+ child: (
+ <>
+
+ {t('Close')}
+ >
+ ),
+ onClick: () => handleWorkOrder(item.orderId, 'close')
+ },
+ {
+ child: (
+ <>
+
+ {t('Delete')}
+ >
+ ),
+ onClick: () => handleWorkOrder(item.orderId, 'delete')
+ }
+ ]}
+ />
+
+ )
+ }
+ ],
+ [handleWorkOrder, router, t]
+ );
+
+ return ;
+};
+
+export default OrderList;
diff --git a/frontend/providers/workorder/src/pages/workorders/index.tsx b/frontend/providers/workorder/src/pages/workorders/index.tsx
new file mode 100644
index 000000000..c663b3df3
--- /dev/null
+++ b/frontend/providers/workorder/src/pages/workorders/index.tsx
@@ -0,0 +1,180 @@
+import { getWorkOrderList } from '@/api/workorder';
+import MyIcon from '@/components/Icon';
+import Pagination from '@/components/Pagination';
+import MySelect from '@/components/Select';
+import SelectDateRange from '@/components/SelectDateRange';
+import Tabs from '@/components/Tabs';
+import { OrderTypeList } from '@/constants/workorder';
+import { useLoading } from '@/hooks/useLoading';
+import { WorkOrderStatus, WorkOrderType } from '@/types/workorder';
+import { serviceSideProps } from '@/utils/i18n';
+import { Box, Button, Flex, Image, Text } from '@chakra-ui/react';
+import { useQuery } from '@tanstack/react-query';
+import { useTranslation } from 'next-i18next';
+import router from 'next/router';
+import { useState } from 'react';
+import List from './components/List';
+import useSessionStore from '@/store/session';
+import useStore from '@/hooks/useStore';
+
+function Home() {
+ const { Loading } = useLoading();
+ const { t } = useTranslation();
+ const [initialized, setInitialized] = useState(false);
+ const [page, setPage] = useState(1);
+ const [pageSize, setPageSize] = useState(10);
+ const [orderStatus, setOrderStatus] = useState(WorkOrderStatus.All);
+ const [orderType, setOrderType] = useState(WorkOrderType.All);
+ const [startTime, setStartTime] = useState(() => {
+ const currentDate = new Date();
+ currentDate.setMonth(currentDate.getMonth() - 1);
+ return currentDate;
+ });
+ const [endTime, setEndTime] = useState(new Date());
+ const user = useStore(useSessionStore, (state) => state.session?.user);
+
+ const { data, refetch } = useQuery(
+ ['getWorkOrderList', page, pageSize, orderStatus, orderType, startTime, endTime],
+ () =>
+ getWorkOrderList({
+ page,
+ pageSize,
+ orderStatus,
+ orderType,
+ startTime,
+ endTime
+ }),
+ {
+ enabled: !!user,
+ refetchInterval: 6000,
+ onSettled() {
+ setInitialized(true);
+ }
+ }
+ );
+
+ return (
+
+
+
+
+
+
+ {t('Order List')}
+
+
+ ({data?.totalCount})
+
+
+ {!user?.isAdmin && (
+ }
+ onClick={() => router.push('/workorder/create')}
+ >
+ {t('New Order')}
+
+ )}
+
+
+
+ {
+ setPage(1);
+ setOrderStatus(id);
+ }}
+ />
+
+ {t('Create Time')}
+
+
+
+ {t('Question Type')}
+
+ {
+ setPage(1);
+ setOrderType(val);
+ }}
+ />
+
+
+ {data?.orders && data?.orders?.length > 0 ? (
+
+
+
+ ) : (
+
+
+ {t('Order Empty')}
+
+ )}
+
+ {!!data?.totalCount && (
+
+ )}
+
+ );
+}
+
+export async function getServerSideProps(content: any) {
+ return {
+ props: {
+ ...(await serviceSideProps(content))
+ }
+ };
+}
+
+export default Home;
diff --git a/frontend/providers/workorder/src/services/backend/auth.ts b/frontend/providers/workorder/src/services/backend/auth.ts
new file mode 100644
index 000000000..a110acd76
--- /dev/null
+++ b/frontend/providers/workorder/src/services/backend/auth.ts
@@ -0,0 +1,47 @@
+import { AppTokenPayload, DesktopTokenPayload } from '@/types/user';
+import { verify, sign } from 'jsonwebtoken';
+import type { NextApiRequest } from 'next';
+import { ERROR_ENUM } from '../error';
+
+const desktopJwtSecret = (process.env.JWT_SECRET_DESKTOP_TO_APP as string) || '123456789';
+const appJwtSecret = (process.env.JWT_SECRET_SELF as string) || '123456789';
+
+export const verifyAccessToken = async (req: NextApiRequest) => {
+ if (!req.headers) return Promise.reject(ERROR_ENUM.unAuthorization);
+ const { authorization } = req.headers;
+ if (!authorization) return Promise.reject(ERROR_ENUM.unAuthorization);
+ return verifyAppToken(authorization);
+};
+
+export const verifyDesktopToken = (token: string) =>
+ new Promise((resolve) => {
+ verify(token, desktopJwtSecret, (err, payload) => {
+ if (err) {
+ console.log(err);
+ resolve(null);
+ } else if (!payload) {
+ console.log('payload is null');
+ resolve(null);
+ } else {
+ resolve(payload as T);
+ }
+ });
+ });
+
+export const verifyAppToken = (token: string) =>
+ new Promise((resolve, reject) => {
+ verify(token, appJwtSecret, (err, payload) => {
+ if (err) {
+ console.log(err);
+ resolve(null);
+ } else if (!payload) {
+ console.log('payload is null');
+ resolve(null);
+ } else {
+ resolve(payload as T);
+ }
+ });
+ });
+
+export const generateAccessToken = (props: AppTokenPayload) =>
+ sign(props, appJwtSecret, { expiresIn: '7d' });
diff --git a/frontend/providers/workorder/src/services/backend/kubernetes.ts b/frontend/providers/workorder/src/services/backend/kubernetes.ts
new file mode 100644
index 000000000..02f6e133a
--- /dev/null
+++ b/frontend/providers/workorder/src/services/backend/kubernetes.ts
@@ -0,0 +1,276 @@
+import * as k8s from '@kubernetes/client-node';
+import * as yaml from 'js-yaml';
+
+export function K8sApiDefault(): k8s.KubeConfig {
+ const kc = new k8s.KubeConfig();
+ kc.loadFromDefault();
+ return kc;
+}
+
+export function CheckIsInCluster(): [boolean, string] {
+ if (
+ process.env.KUBERNETES_SERVICE_HOST !== undefined &&
+ process.env.KUBERNETES_SERVICE_HOST !== '' &&
+ process.env.KUBERNETES_SERVICE_PORT !== undefined &&
+ process.env.KUBERNETES_SERVICE_PORT !== ''
+ ) {
+ return [
+ true,
+ 'https://' + process.env.KUBERNETES_SERVICE_HOST + ':' + process.env.KUBERNETES_SERVICE_PORT
+ ];
+ }
+ return [false, ''];
+}
+
+/* init api */
+export function K8sApi(config = ''): k8s.KubeConfig {
+ const kc = new k8s.KubeConfig();
+ config ? kc.loadFromString(config) : kc.loadFromCluster();
+
+ const cluster = kc.getCurrentCluster();
+
+ if (cluster !== null) {
+ let server: k8s.Cluster;
+
+ const [inCluster, hosts] = CheckIsInCluster();
+ if (inCluster && hosts !== '') {
+ server = {
+ name: cluster.name,
+ caData: cluster.caData,
+ caFile: cluster.caFile,
+ server: hosts,
+ skipTLSVerify: cluster.skipTLSVerify
+ };
+ } else {
+ server = {
+ name: cluster.name,
+ caData: cluster.caData,
+ caFile: cluster.caFile,
+ server: 'https://apiserver.cluster.local:6443',
+ skipTLSVerify: cluster.skipTLSVerify
+ };
+ }
+ kc.clusters.forEach((item, i) => {
+ if (item.name === cluster.name) {
+ kc.clusters[i] = server;
+ }
+ });
+ }
+
+ return kc;
+}
+
+export type CRDMeta = {
+ group: string; // group
+ version: string; // version
+ namespace: string; // namespace
+ plural: string; // type
+};
+
+export async function createYaml(
+ kc: k8s.KubeConfig,
+ specs: k8s.KubernetesObject[]
+): Promise {
+ const client = k8s.KubernetesObjectApi.makeApiClient(kc);
+ const validSpecs = specs.filter((s) => s && s.kind && s.metadata);
+ const created = [] as k8s.KubernetesObject[];
+
+ try {
+ for (const spec of validSpecs) {
+ spec.metadata = spec.metadata || {};
+ spec.metadata.annotations = spec.metadata.annotations || {};
+ delete spec.metadata.annotations['kubectl.kubernetes.io/last-applied-configuration'];
+ spec.metadata.annotations['kubectl.kubernetes.io/last-applied-configuration'] =
+ JSON.stringify(spec);
+ console.log('create yaml: ', spec.kind);
+ const response = await client.create(spec);
+ created.push(response.body);
+ }
+ } catch (error: any) {
+ console.log('create error');
+ /* delete success specs */
+ for (const spec of created) {
+ try {
+ console.log('delete:', spec.kind);
+ client.delete(spec);
+ } catch (error) {
+ error;
+ }
+ }
+ // console.error(error, '<=create error')
+ return Promise.reject(error);
+ }
+ return created;
+}
+
+export async function updateYaml(
+ kc: k8s.KubeConfig,
+ specs: k8s.KubernetesObject[],
+ canCreate = false
+): Promise {
+ const client = k8s.KubernetesObjectApi.makeApiClient(kc);
+ const validSpecs = specs.filter((s) => s && s.kind && s.metadata);
+ const created = [] as k8s.KubernetesObject[];
+
+ try {
+ for (const spec of validSpecs) {
+ spec.metadata = spec.metadata || {};
+ spec.metadata.annotations = spec.metadata.annotations || {};
+ delete spec.metadata.annotations['kubectl.kubernetes.io/last-applied-configuration'];
+ spec.metadata.annotations['kubectl.kubernetes.io/last-applied-configuration'] =
+ JSON.stringify(spec);
+
+ console.log('update yaml: ', spec.kind);
+ const response = await client.create(spec);
+ created.push(response.body);
+ }
+ } catch (error: any) {
+ /* delete success specs */
+ for (const spec of validSpecs) {
+ try {
+ console.log('delete:', spec.kind);
+ await client.delete(spec);
+ } catch (error) {
+ error;
+ }
+ }
+ if (error.body.reason === 'AlreadyExists' && canCreate) {
+ return updateYaml(kc, specs);
+ }
+ return Promise.reject(error);
+ }
+ return created;
+}
+
+export async function replaceYaml(
+ kc: k8s.KubeConfig,
+ specs: k8s.KubernetesObject[]
+): Promise {
+ const client = k8s.KubernetesObjectApi.makeApiClient(kc);
+ const validSpecs = specs.filter((s) => s && s.kind && s.metadata);
+ const succeed = [] as k8s.KubernetesObject[];
+
+ for (const spec of validSpecs) {
+ spec.metadata = spec.metadata || {};
+ spec.metadata.annotations = spec.metadata.annotations || {};
+ delete spec.metadata.annotations['kubectl.kubernetes.io/last-applied-configuration'];
+ spec.metadata.annotations['kubectl.kubernetes.io/last-applied-configuration'] =
+ JSON.stringify(spec);
+
+ try {
+ // @ts-ignore
+ const { body } = await client.read(spec);
+ console.log('replace yaml: ', spec.kind);
+ // update resource
+ const response = await client.replace({
+ ...spec,
+ metadata: {
+ ...spec.metadata,
+ resourceVersion: body.metadata?.resourceVersion
+ }
+ });
+ succeed.push(response.body);
+ } catch (e: any) {
+ // console.error(e?.body || e, "<=replace error")
+ // no yaml, create it
+ if (e?.body?.code && +e?.body?.code === 404) {
+ try {
+ console.log('create yaml: ', spec.kind);
+ const response = await client.create(spec);
+ succeed.push(response.body);
+ } catch (error: any) {
+ // console.error(error, '<=create error')
+ return Promise.reject(error);
+ }
+ } else {
+ return Promise.reject(e);
+ }
+ }
+ }
+ return succeed;
+}
+
+export async function delYaml(kc: k8s.KubeConfig, specs: k8s.KubernetesObject[]) {
+ const client = k8s.KubernetesObjectApi.makeApiClient(kc);
+ const validSpecs = specs.filter((s) => s && s.kind && s.metadata);
+
+ try {
+ for (const spec of validSpecs) {
+ console.log('delete:', spec.kind);
+ client.delete(spec);
+ }
+ } catch (error: any) {
+ // console.error(error, '<=create error')
+ return Promise.reject(error);
+ }
+}
+
+export function GetUserDefaultNameSpace(user: string): string {
+ return 'ns-' + user;
+}
+
+export async function getK8s({ kubeconfig }: { kubeconfig: string }) {
+ const kc = K8sApi(kubeconfig);
+ const kube_user = kc.getCurrentUser();
+ const client = k8s.KubernetesObjectApi.makeApiClient(kc);
+
+ if (kube_user === null) {
+ return Promise.reject('用户不存在');
+ }
+
+ const namespace = GetUserDefaultNameSpace(kube_user.name);
+
+ const applyYamlList = async (yamlList: string[], type: 'create' | 'replace' | 'update') => {
+ // insert namespace
+ const formatYaml: k8s.KubernetesObject[] = yamlList
+ .map((item) => yaml.loadAll(item))
+ .flat()
+ .map((item: any) => {
+ if (item.metadata) {
+ item.metadata.namespace = namespace;
+ }
+ return item;
+ });
+
+ if (type === 'create') {
+ return createYaml(kc, formatYaml);
+ } else if (type === 'replace') {
+ return replaceYaml(kc, formatYaml);
+ } else if (type === 'update') {
+ return updateYaml(kc, formatYaml, true);
+ }
+ return createYaml(kc, formatYaml);
+ };
+
+ const delYamlList = (yamlList: string[]) => {
+ // insert namespace
+ const formatYaml: k8s.KubernetesObject[] = yamlList
+ .map((item) => yaml.loadAll(item))
+ .flat()
+ .map((item: any) => {
+ if (item.metadata) {
+ item.metadata.namespace = namespace;
+ }
+ return item;
+ });
+ return delYaml(kc, formatYaml);
+ };
+
+ return Promise.resolve({
+ kc,
+ apiClient: client,
+ k8sCore: kc.makeApiClient(k8s.CoreV1Api),
+ k8sApp: kc.makeApiClient(k8s.AppsV1Api),
+ k8sAutoscaling: kc.makeApiClient(k8s.AutoscalingV2Api),
+ k8sNetworkingApp: kc.makeApiClient(k8s.NetworkingV1Api),
+ k8sCustomObjects: kc.makeApiClient(k8s.CustomObjectsApi),
+ k8sEvents: kc.makeApiClient(k8s.EventsV1Api),
+ k8sAuth: kc.makeApiClient(k8s.RbacAuthorizationV1Api),
+ k8sBatch: kc.makeApiClient(k8s.BatchV1Api),
+ metricsClient: new k8s.Metrics(kc),
+ kube_user,
+ namespace,
+ applyYamlList,
+ delYamlList
+ });
+}
diff --git a/frontend/providers/workorder/src/services/backend/response.ts b/frontend/providers/workorder/src/services/backend/response.ts
new file mode 100644
index 000000000..ccbf4b656
--- /dev/null
+++ b/frontend/providers/workorder/src/services/backend/response.ts
@@ -0,0 +1,36 @@
+import { NextApiResponse } from 'next';
+import { ERROR_RESPONSE } from '../error';
+
+export const jsonRes = (
+ res: NextApiResponse,
+ props?: {
+ code?: number;
+ message?: string;
+ data?: T;
+ error?: any;
+ }
+) => {
+ const { code = 200, message = '', data = null, error } = props || {};
+
+ // Specified error
+ if (typeof error === 'string' && ERROR_RESPONSE[error]) {
+ return res.json(ERROR_RESPONSE[error]);
+ }
+
+ // another error
+ let msg = message;
+ if ((code < 200 || code >= 400) && !message) {
+ msg = error?.body?.message || error?.message || '请求错误';
+ if (typeof error === 'string') {
+ msg = error;
+ }
+ console.log('===error===', error?.body || error);
+ }
+
+ res.json({
+ code,
+ statusText: '',
+ message: msg,
+ data: data || error || null
+ });
+};
diff --git a/frontend/providers/workorder/src/services/db/mongodb.ts b/frontend/providers/workorder/src/services/db/mongodb.ts
new file mode 100644
index 000000000..4002b3621
--- /dev/null
+++ b/frontend/providers/workorder/src/services/db/mongodb.ts
@@ -0,0 +1,24 @@
+import { MongoClient } from 'mongodb';
+
+export async function connectToDatabase() {
+ if (global.mongodb) {
+ return global.mongodb;
+ }
+ const uri = process.env.MONGODB_URI || '';
+ global.mongodb = new MongoClient(uri);
+ // global.mongodb = 'connecting';
+ try {
+ global.mongodb.on('error', (err) => {
+ global.mongodb = null;
+ });
+ global.mongodb.on('close', () => {
+ global.mongodb = null;
+ });
+ await global.mongodb.connect();
+ return global.mongodb;
+ } catch (error) {
+ console.log('error->', 'mongo connect error');
+ global.mongodb = null;
+ return Promise.reject(error);
+ }
+}
diff --git a/frontend/providers/workorder/src/services/db/region.ts b/frontend/providers/workorder/src/services/db/region.ts
new file mode 100644
index 000000000..27ea056b1
--- /dev/null
+++ b/frontend/providers/workorder/src/services/db/region.ts
@@ -0,0 +1,14 @@
+import { RegionDB } from '@/types/region';
+import { connectToDatabase } from './mongodb';
+
+async function connectUserCollection() {
+ const client = await connectToDatabase();
+ const collection = client.db().collection('region');
+ return collection;
+}
+
+export async function getRegionById(sealosRegionUid: string) {
+ const collection = await connectUserCollection();
+ const region = await collection.findOne({ sealosRegionUid });
+ return region;
+}
diff --git a/frontend/providers/workorder/src/services/db/user.ts b/frontend/providers/workorder/src/services/db/user.ts
new file mode 100644
index 000000000..ea452c2f4
--- /dev/null
+++ b/frontend/providers/workorder/src/services/db/user.ts
@@ -0,0 +1,32 @@
+import { UserDB } from '@/types/user';
+import { connectToDatabase } from './mongodb';
+
+async function connectUserCollection() {
+ const client = await connectToDatabase();
+ const collection = client.db().collection('user');
+ return collection;
+}
+
+export async function createUser(user: UserDB) {
+ const collection = await connectUserCollection();
+ const result = await collection.insertOne(user, {});
+ return result;
+}
+
+export async function getUserById(userId: string) {
+ const collection = await connectUserCollection();
+ const user = await collection.findOne({ userId });
+ return user;
+}
+
+export async function updateUser(userId: string, updates: Partial) {
+ const collection = await connectUserCollection();
+ const result = await collection.updateOne({ userId }, { $set: updates });
+ return result;
+}
+
+export async function deleteUser(userId: string) {
+ const collection = await connectUserCollection();
+ const result = await collection.deleteOne({ userId });
+ return result;
+}
diff --git a/frontend/providers/workorder/src/services/db/workorder.ts b/frontend/providers/workorder/src/services/db/workorder.ts
new file mode 100644
index 000000000..6cffb1e14
--- /dev/null
+++ b/frontend/providers/workorder/src/services/db/workorder.ts
@@ -0,0 +1,156 @@
+import { WorkOrderDB, WorkOrderDialog, WorkOrderStatus, WorkOrderType } from '@/types/workorder';
+import { connectToDatabase } from './mongodb';
+import { getUserById } from './user';
+
+async function connectOrderCollection() {
+ const client = await connectToDatabase();
+ const collection = client.db().collection('order');
+ return collection;
+}
+
+async function isAdminFilter({ userId, orderId }: { userId: string; orderId: string }) {
+ const user = await getUserById(userId);
+ if (user?.isAdmin) {
+ return { orderId };
+ }
+ return { orderId, userId };
+}
+
+export async function createOrder({ order }: { order: WorkOrderDB }) {
+ const collection = await connectOrderCollection();
+ const result = await collection.insertOne(order);
+ return result;
+}
+
+export async function getAllOrdersByUserId({
+ userId,
+ page,
+ pageSize,
+ orderType = WorkOrderType.All,
+ startTime,
+ endTime,
+ orderStatus = WorkOrderStatus.All
+}: {
+ userId: string;
+ page: number;
+ pageSize: number;
+ orderType?: WorkOrderType;
+ startTime?: Date;
+ endTime?: Date;
+ orderStatus?: WorkOrderStatus;
+}) {
+ const collection = await connectOrderCollection();
+
+ const skip = (page - 1) * pageSize;
+ const user = await getUserById(userId);
+
+ const baseQuery = user?.isAdmin ? {} : { userId, status: { $ne: WorkOrderStatus.Deleted } };
+
+ let typeQuery = {};
+ if (orderType !== 'all') {
+ typeQuery = { type: orderType };
+ }
+
+ let timeQuery = {};
+ if (startTime && endTime) {
+ timeQuery = {
+ createTime: {
+ $gte: new Date(startTime),
+ $lte: new Date(endTime)
+ }
+ };
+ }
+
+ let statusQuery = {};
+ if (orderStatus !== 'all') {
+ statusQuery = { status: orderStatus };
+ }
+
+ const query = { ...baseQuery, ...typeQuery, ...timeQuery, ...statusQuery };
+
+ const options = {
+ skip: skip,
+ limit: pageSize,
+ sort: { createTime: -1 } as Record,
+ projection: { _id: 0 }
+ };
+
+ const orders = await collection.find(query, options).toArray();
+ const totalCount = await collection.countDocuments(query);
+
+ return { orders, totalCount };
+}
+
+export async function getOrderByOrderIdAndUserId({
+ orderId,
+ userId
+}: {
+ orderId: string;
+ userId: string;
+}) {
+ const collection = await connectOrderCollection();
+ const filter = await isAdminFilter({ orderId, userId });
+ const order = await collection.findOne(filter);
+ return order;
+}
+
+export async function updateOrder({
+ orderId,
+ userId,
+ updates
+}: {
+ orderId: string;
+ userId: string;
+ updates: Partial;
+}) {
+ const collection = await connectOrderCollection();
+
+ const filter = await isAdminFilter({ orderId, userId });
+ const update = {
+ $set: {
+ ...updates,
+ updateTime: new Date()
+ }
+ };
+
+ const result = await collection.updateOne(filter, update);
+ return result;
+}
+
+export async function addDialogToOrder({
+ orderId,
+ userId,
+ dialog
+}: {
+ orderId: string;
+ userId: string;
+ dialog: WorkOrderDialog;
+}) {
+ const collection = await connectOrderCollection();
+
+ const filter = await isAdminFilter({ orderId, userId });
+ const update = {
+ $push: {
+ dialogs: dialog
+ }
+ };
+
+ const result = await collection.updateOne(filter, update);
+
+ return result;
+}
+
+export async function deleteOrder({ orderId, userId }: { orderId: string; userId: string }) {
+ const collection = await connectOrderCollection();
+
+ const filter = await isAdminFilter({ orderId, userId });
+ const update = {
+ $set: {
+ status: 'deleted' as WorkOrderStatus,
+ updateTime: new Date()
+ }
+ };
+
+ const result = await collection.updateOne(filter, update);
+ return result;
+}
diff --git a/frontend/providers/workorder/src/services/error.ts b/frontend/providers/workorder/src/services/error.ts
new file mode 100644
index 000000000..058cfe09b
--- /dev/null
+++ b/frontend/providers/workorder/src/services/error.ts
@@ -0,0 +1,19 @@
+export enum ERROR_ENUM {
+ unAuthorization = 'unAuthorization'
+}
+
+export const ERROR_RESPONSE: Record<
+ any,
+ {
+ code: number;
+ statusText: string;
+ message: string;
+ data?: any;
+ }
+> = {
+ [ERROR_ENUM.unAuthorization]: {
+ code: 403,
+ statusText: ERROR_ENUM.unAuthorization,
+ message: '凭证错误'
+ }
+};
diff --git a/frontend/providers/workorder/src/services/kubernet.ts b/frontend/providers/workorder/src/services/kubernet.ts
new file mode 100644
index 000000000..48413378f
--- /dev/null
+++ b/frontend/providers/workorder/src/services/kubernet.ts
@@ -0,0 +1,50 @@
+export interface ApiResp {
+ code: number;
+ message: string;
+ data?: T;
+}
+
+export const isApiResp = (x: any): x is ApiResp =>
+ typeof x.code === 'number' && typeof x.message === 'string';
+
+export const showStatus = (status: number) => {
+ let message = '';
+ switch (status) {
+ case 400:
+ message = '请求错误(400)';
+ break;
+ case 401:
+ message = '未授权,请重新登录(401)';
+ break;
+ case 403:
+ message = '拒绝访问(403)';
+ break;
+ case 404:
+ message = '请求出错(404)';
+ break;
+ case 408:
+ message = '请求超时(408)';
+ break;
+ case 500:
+ message = '服务器错误(500)';
+ break;
+ case 501:
+ message = '服务未实现(501)';
+ break;
+ case 502:
+ message = '网络错误(502)';
+ break;
+ case 503:
+ message = '服务不可用(503)';
+ break;
+ case 504:
+ message = '网络超时(504)';
+ break;
+ case 505:
+ message = 'HTTP版本不受支持(505)';
+ break;
+ default:
+ message = `连接出错(${status})!`;
+ }
+ return `${message},请检查网络或联系管理员!`;
+};
diff --git a/frontend/providers/workorder/src/services/request.ts b/frontend/providers/workorder/src/services/request.ts
new file mode 100644
index 000000000..7c40c5d17
--- /dev/null
+++ b/frontend/providers/workorder/src/services/request.ts
@@ -0,0 +1,95 @@
+import useSessionStore from '@/store/session';
+import axios, {
+ AxiosHeaders,
+ AxiosRequestConfig,
+ AxiosResponse,
+ InternalAxiosRequestConfig
+} from 'axios';
+import type { ApiResp } from './kubernet';
+import { isApiResp, showStatus } from './kubernet';
+
+const request = axios.create({
+ baseURL: '/',
+ withCredentials: true,
+ timeout: 60000
+});
+
+// request interceptor
+request.interceptors.request.use(
+ (config: InternalAxiosRequestConfig) => {
+ let _headers: AxiosHeaders = config.headers;
+
+ _headers['Authorization'] = useSessionStore.getState()?.session?.token;
+
+ if (!config.headers || config.headers['Content-Type'] === '') {
+ _headers['Content-Type'] = 'application/json';
+ }
+
+ config.headers = _headers;
+ return config;
+ },
+ (error: any) => {
+ error.data = {};
+ error.data.msg = '服务器异常,请联系管理员!';
+ return Promise.resolve(error);
+ }
+);
+
+// response interceptor
+request.interceptors.response.use(
+ (response: AxiosResponse) => {
+ const { status, data } = response;
+
+ if (status < 200 || status >= 300 || !isApiResp(data)) {
+ return Promise.reject(
+ status + ':' + showStatus(status) + ', ' + typeof data === 'string' ? data : String(data)
+ );
+ }
+
+ const apiResp = data as ApiResp;
+ if (apiResp.code < 200 || apiResp.code >= 400) {
+ return Promise.reject(apiResp);
+ }
+
+ response.data = apiResp.data;
+ return response.data;
+ },
+ (error: any) => {
+ if (axios.isCancel(error)) {
+ return Promise.reject('cancel request' + String(error));
+ } else {
+ error.errMessage = '请求超时或服务器异常,请检查网络或联系管理员!';
+ }
+ return Promise.reject(error);
+ }
+);
+
+export function GET(
+ url: string,
+ data?: { [key: string]: any },
+ config?: AxiosRequestConfig
+): Promise {
+ return request.get(url, {
+ params: data,
+ ...config
+ });
+}
+
+export function POST(
+ url: string,
+ data?: { [key: string]: any },
+ config?: AxiosRequestConfig
+): Promise {
+ return request.post(url, data, config);
+}
+
+export function DELETE(
+ url: string,
+ data?: { [key: string]: any },
+ config?: AxiosRequestConfig
+): Promise {
+ return request.delete(url, {
+ params: data,
+ ...config
+ });
+}
diff --git a/frontend/providers/workorder/src/store/env.ts b/frontend/providers/workorder/src/store/env.ts
new file mode 100644
index 000000000..7fb176807
--- /dev/null
+++ b/frontend/providers/workorder/src/store/env.ts
@@ -0,0 +1,32 @@
+import { getSystemEnv } from '@/api/platform';
+import { SystemEnvResponse } from '@/pages/api/platform/getEnv';
+import { create } from 'zustand';
+import { immer } from 'zustand/middleware/immer';
+
+export let Domain: string | undefined;
+
+type EnvState = {
+ SystemEnv: SystemEnvResponse;
+ initSystemEnv: () => Promise;
+};
+
+const useEnvStore = create()(
+ immer((set, get) => ({
+ SystemEnv: {
+ domain: '',
+ env_storage_className: '',
+ migrate_file_image: '',
+ minio_url: ''
+ },
+ initSystemEnv: async () => {
+ const data = await getSystemEnv();
+ Domain = data.domain;
+ set((state) => {
+ state.SystemEnv = data;
+ });
+ return data;
+ }
+ }))
+);
+
+export default useEnvStore;
diff --git a/frontend/providers/workorder/src/store/global.ts b/frontend/providers/workorder/src/store/global.ts
new file mode 100644
index 000000000..c638165fa
--- /dev/null
+++ b/frontend/providers/workorder/src/store/global.ts
@@ -0,0 +1,37 @@
+import { create } from 'zustand';
+import { devtools } from 'zustand/middleware';
+import { immer } from 'zustand/middleware/immer';
+
+type State = {
+ screenWidth: number;
+ setScreenWidth: (e: number) => void;
+ loading: boolean;
+ setLoading: (val: boolean) => void;
+ lastRoute: string;
+ setLastRoute: (val: string) => void;
+};
+
+export const useGlobalStore = create()(
+ devtools(
+ immer((set, get) => ({
+ screenWidth: 1440,
+ setScreenWidth(e: number) {
+ set((state) => {
+ state.screenWidth = e;
+ });
+ },
+ loading: false,
+ setLoading(val: boolean) {
+ set((state) => {
+ state.loading = val;
+ });
+ },
+ lastRoute: '/',
+ setLastRoute(val) {
+ set((state) => {
+ state.lastRoute = val;
+ });
+ }
+ }))
+ )
+);
diff --git a/frontend/providers/workorder/src/store/session.ts b/frontend/providers/workorder/src/store/session.ts
new file mode 100644
index 000000000..41eec5f36
--- /dev/null
+++ b/frontend/providers/workorder/src/store/session.ts
@@ -0,0 +1,40 @@
+import { AuthByDesktopSession } from '@/api/user';
+import { AppSession } from '@/types/user';
+import { create } from 'zustand';
+import { immer } from 'zustand/middleware/immer';
+import { persist } from 'zustand/middleware';
+
+export const sessionKey = 'session';
+
+type State = {
+ session: AppSession | null;
+ authUser: (token: string) => Promise;
+ setAppSession: (session: AppSession) => void;
+ delAppSession: () => void;
+};
+
+const useSessionStore = create()(
+ persist(
+ immer((set, get) => ({
+ session: null,
+ authUser: async (token) => {
+ const data = await AuthByDesktopSession({ token });
+ set((state) => {
+ state.session = data;
+ });
+ return data;
+ },
+ setAppSession: (session) => {
+ set({ session });
+ },
+ delAppSession: () => {
+ set({ session: null });
+ }
+ })),
+ {
+ name: sessionKey
+ }
+ )
+);
+
+export default useSessionStore;
diff --git a/frontend/providers/workorder/src/styles/reset.scss b/frontend/providers/workorder/src/styles/reset.scss
new file mode 100644
index 000000000..7ac926de6
--- /dev/null
+++ b/frontend/providers/workorder/src/styles/reset.scss
@@ -0,0 +1,151 @@
+body,
+h1,
+h2,
+h3,
+h4,
+hr,
+p,
+blockquote,
+dl,
+dt,
+dd,
+ul,
+ol,
+li,
+pre,
+form,
+fieldset,
+legend,
+button,
+input,
+textarea,
+th,
+td {
+ margin: 0;
+}
+body,
+button,
+input,
+select,
+textarea {
+ font: 12px/1.5tahoma, arial, \5b8b\4f53;
+}
+// h1, h2, h3, h4, h5, h6{ font-size:100%; }
+address,
+cite,
+dfn,
+em,
+var {
+ font-style: normal;
+}
+
+small {
+ font-size: 12px;
+}
+ul,
+ol {
+ list-style: none;
+ padding: 0;
+}
+a {
+ text-decoration: none;
+}
+a:hover {
+ text-decoration: underline;
+}
+sup {
+ vertical-align: text-top;
+}
+sub {
+ vertical-align: text-bottom;
+}
+legend {
+ color: #000;
+}
+fieldset,
+img {
+ border: 0;
+}
+button,
+input,
+select,
+textarea {
+ font-size: 100%;
+}
+table {
+ border-collapse: collapse;
+ border-spacing: 0;
+}
+
+* {
+ box-sizing: border-box;
+}
+
+.icon {
+ width: 1em;
+ height: 1em;
+ vertical-align: -0.15em;
+ fill: currentColor;
+ overflow: hidden;
+}
+
+input::placeholder,
+textarea::placeholder {
+ color: var(--chakra-colors-blackAlpha-400);
+}
+
+#__next {
+ height: 100%;
+}
+
+::-webkit-scrollbar,
+::-webkit-scrollbar {
+ width: 6px;
+ height: 6px;
+}
+::-webkit-scrollbar-track,
+::-webkit-scrollbar-track {
+ background: transparent !important;
+ border-radius: 2px;
+}
+::-webkit-scrollbar-thumb,
+::-webkit-scrollbar-thumb {
+ background: rgba(189, 193, 197, 1) !important;
+ border-radius: 2px;
+}
+::-webkit-scrollbar-thumb:hover,
+::-webkit-scrollbar-thumb:hover {
+ background: rgba(189, 193, 197, 1) !important;
+}
+
+div {
+ &::-webkit-scrollbar-thumb,
+ &::-webkit-scrollbar-thumb {
+ background: transparent !important;
+ border-radius: 2px;
+ transition: 1s;
+ }
+ &:hover {
+ &::-webkit-scrollbar-thumb,
+ &::-webkit-scrollbar-thumb {
+ background: rgba(189, 193, 197, 0.5) !important;
+ }
+ &::-webkit-scrollbar-thumb:hover,
+ &::-webkit-scrollbar-thumb:hover {
+ background: rgba(189, 193, 197, 1) !important;
+ }
+ }
+}
+
+div.rdp {
+ --rdp-cell-size: 40px;
+ --rdp-accent-color: black;
+ --rdp-background-color: #f4f6f8;
+ /* Switch to dark colors for dark themes */
+ // --rdp-accent-color-dark: #3003e1;
+ // --rdp-background-color-dark: #180270;
+ /* Outline border for focused elements */
+ --rdp-outline: 2px solid var(--rdp-accent-color);
+ /* Outline border for focused and selected elements */
+ --rdp-outline-selected: 2px solid rgba(0, 0, 0, 0.75);
+}
diff --git a/frontend/providers/workorder/src/types/index.ts b/frontend/providers/workorder/src/types/index.ts
new file mode 100644
index 000000000..20a24318e
--- /dev/null
+++ b/frontend/providers/workorder/src/types/index.ts
@@ -0,0 +1,5 @@
+import { MongoClient } from 'mongodb';
+
+declare global {
+ var mongodb: MongoClient | null;
+}
diff --git a/frontend/providers/workorder/src/types/region.ts b/frontend/providers/workorder/src/types/region.ts
new file mode 100644
index 000000000..798b9ca8d
--- /dev/null
+++ b/frontend/providers/workorder/src/types/region.ts
@@ -0,0 +1,5 @@
+export type RegionDB = {
+ name: string;
+ sealosRegionUid: string; // id
+ sealosRegionDomain: string;
+};
diff --git a/frontend/providers/workorder/src/types/user.ts b/frontend/providers/workorder/src/types/user.ts
new file mode 100644
index 000000000..6d2fef295
--- /dev/null
+++ b/frontend/providers/workorder/src/types/user.ts
@@ -0,0 +1,24 @@
+export type DesktopTokenPayload = {
+ regionUid: string;
+ userCrUid: string;
+ userCrName: string;
+ workspaceUid: string;
+ workspaceId: string;
+ userUid: string;
+ userId: string; // only
+ iat: number;
+ exp: number;
+};
+
+export type UserDB = Omit & {
+ isAdmin: boolean;
+};
+
+export type AppTokenPayload = Omit & {
+ isAdmin: boolean;
+};
+
+export type AppSession = {
+ token: string;
+ user: AppTokenPayload;
+};
diff --git a/frontend/providers/workorder/src/types/workorder.ts b/frontend/providers/workorder/src/types/workorder.ts
new file mode 100644
index 000000000..647cbf6b4
--- /dev/null
+++ b/frontend/providers/workorder/src/types/workorder.ts
@@ -0,0 +1,56 @@
+import { ObjectId } from 'mongodb';
+import { UserDB } from './user';
+
+export type WorkOrderDB = {
+ _id?: ObjectId; // only
+ userId: string; // only
+ orderId: string; // only
+ type: WorkOrderType;
+ createTime: Date;
+ updateTime: Date;
+ completeTime?: Date;
+ description: string;
+ status: WorkOrderStatus;
+ dialogs?: WorkOrderDialog[];
+ appendix?: string[];
+ manualHandling: {
+ isManuallyHandled: boolean;
+ handlingTime?: Date;
+ };
+ userInfo: UserDB;
+};
+
+export type WorkOrderDialog = {
+ time: Date;
+ content: string;
+ userId: string;
+ isAdmin: boolean;
+ isAIBot: boolean;
+};
+
+export enum WorkOrderType {
+ All = 'all',
+ AppLaunchpad = 'applaunchpad',
+ DBProvider = 'dbprovider',
+ CostCenter = 'costcenter',
+ CronJob = 'cronjob',
+ Template = 'template',
+ Cronjob = 'cronjob',
+ CloudServer = 'cloudserver',
+ ObjectStorage = 'objectstorage',
+ FunctionServer = 'functionserver',
+ Other = 'other'
+}
+
+export type WorkOrderEditForm = {
+ type: WorkOrderType;
+ description: string;
+};
+
+export enum WorkOrderStatus {
+ All = 'all',
+ Pending = 'pending',
+ Processing = 'processing',
+ Completed = 'completed',
+ Deleted = 'deleted'
+}
diff --git a/frontend/providers/workorder/src/utils/cookieUtils.ts b/frontend/providers/workorder/src/utils/cookieUtils.ts
new file mode 100644
index 000000000..be69f2aaf
--- /dev/null
+++ b/frontend/providers/workorder/src/utils/cookieUtils.ts
@@ -0,0 +1,15 @@
+import Cookies from 'js-cookie';
+
+export const LANG_KEY = 'NEXT_LOCALE';
+
+export const setLangStore = (value: string) => {
+ return Cookies.set(LANG_KEY, value, { expires: 30, sameSite: 'None', secure: true });
+};
+
+export const getLangStore = () => {
+ return Cookies.get(LANG_KEY);
+};
+
+export const removeLangStore = () => {
+ Cookies.remove(LANG_KEY);
+};
diff --git a/frontend/providers/workorder/src/utils/crypto.ts b/frontend/providers/workorder/src/utils/crypto.ts
new file mode 100644
index 000000000..7d78b7f3b
--- /dev/null
+++ b/frontend/providers/workorder/src/utils/crypto.ts
@@ -0,0 +1,7 @@
+import * as crypto from 'crypto';
+
+export const hashCrypto = (text: string): string => {
+ const hash = crypto.createHash('sha256');
+ hash.update(text);
+ return hash.digest('hex');
+};
diff --git a/frontend/providers/workorder/src/utils/file.ts b/frontend/providers/workorder/src/utils/file.ts
new file mode 100644
index 000000000..7c2000602
--- /dev/null
+++ b/frontend/providers/workorder/src/utils/file.ts
@@ -0,0 +1,77 @@
+/**
+ * 读取 txt 文件内容
+ */
+export const readTxtContent = (file: File) => {
+ return new Promise((resolve: (_: string) => void, reject) => {
+ try {
+ const reader = new FileReader();
+ reader.onload = () => {
+ resolve(reader.result as string);
+ };
+ reader.onerror = (err) => {
+ console.log('error txt read:', err);
+ reject('读取 txt 文件失败');
+ };
+ reader.readAsText(file);
+ } catch (error) {
+ reject('浏览器不支持文件内容读取');
+ }
+ });
+};
+
+/**
+ * file download
+ */
+export const fileDownload = ({
+ text,
+ type,
+ filename
+}: {
+ text: string;
+ type: string;
+ filename: string;
+}) => {
+ const blob = new Blob([`\uFEFF${text}`], { type: `${type};charset=utf-8;` });
+
+ const downloadLink = document.createElement('a');
+ downloadLink.href = window.URL.createObjectURL(blob);
+ downloadLink.download = filename;
+
+ document.body.appendChild(downloadLink);
+ downloadLink.click();
+ document.body.removeChild(downloadLink);
+};
+
+export const fileToBase64 = (file: File) => {
+ return new Promise((resolve, reject) => {
+ const reader = new FileReader();
+ reader.readAsDataURL(file);
+ reader.onload = () => resolve(reader.result);
+ reader.onerror = (error) => reject(error);
+ });
+};
+
+/* simple text, remove chinese space and extra \n */
+export const simpleText = (text: string) => {
+ text = text.replace(/([\u4e00-\u9fa5])\s+([\u4e00-\u9fa5])/g, '$1$2');
+ text = text.replace(/\n{2,}/g, '\n');
+ text = text.replace(/\s{2,}/g, ' ');
+
+ text = text.replace(/[\x00-\x1F]/g, ' ');
+
+ return text;
+};
+
+export const isURL = (str: string) => {
+ const urlRegex = /^(https?|ftp):\/\/[^\s/$.?#].[^\s]*$/i;
+ return urlRegex.test(str);
+};
+
+export const downloadFileByUrl = (url: string, fileName: string) => {
+ const link = document.createElement('a');
+ link.href = url;
+ link.download = fileName;
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+};
diff --git a/frontend/providers/workorder/src/utils/i18n.ts b/frontend/providers/workorder/src/utils/i18n.ts
new file mode 100644
index 000000000..8e6d1544a
--- /dev/null
+++ b/frontend/providers/workorder/src/utils/i18n.ts
@@ -0,0 +1,11 @@
+import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
+import { LANG_KEY } from './cookieUtils';
+
+export const serviceSideProps = (content: any) => {
+ return serverSideTranslations(
+ content.req.cookies[LANG_KEY] || 'en',
+ undefined,
+ null,
+ content.locales
+ );
+};
diff --git a/frontend/providers/workorder/src/utils/tools.ts b/frontend/providers/workorder/src/utils/tools.ts
new file mode 100644
index 000000000..df4832051
--- /dev/null
+++ b/frontend/providers/workorder/src/utils/tools.ts
@@ -0,0 +1,292 @@
+import { useToast } from '@/hooks/useToast';
+import { addHours, format, set, startOfDay } from 'date-fns';
+import dayjs from 'dayjs';
+import { useTranslation } from 'next-i18next';
+
+export const formatTime = (time: string | number | Date, format = 'YYYY-MM-DD HH:mm:ss') => {
+ return dayjs(time).format(format);
+};
+
+export function addHoursToTime(inputTimeStr: string, hoursToAdd = 8) {
+ const inputTime = new Date(inputTimeStr);
+ inputTime.setHours(inputTime.getHours() + hoursToAdd);
+ return formatTime(inputTime);
+}
+/**
+ * copy text data
+ */
+export const useCopyData = () => {
+ const { toast } = useToast();
+ const { t } = useTranslation();
+
+ return {
+ copyData: (data: string, title: string = 'Copy Success') => {
+ try {
+ const textarea = document.createElement('textarea');
+ textarea.value = data;
+ document.body.appendChild(textarea);
+ textarea.select();
+ document.execCommand('copy');
+ document.body.removeChild(textarea);
+ toast({
+ title: t(title),
+ status: 'success',
+ duration: 1000
+ });
+ } catch (error) {
+ console.error(error);
+ toast({
+ title: t('Copy Failed'),
+ status: 'error'
+ });
+ }
+ }
+ };
+};
+
+/**
+ * format string to number or ''
+ */
+export const str2Num = (str?: string | number) => {
+ return !!str ? +str : 0;
+};
+
+/**
+ * add ./ in path
+ */
+export const pathFormat = (str: string) => {
+ if (str.startsWith('/')) return `.${str}`;
+ return `./${str}`;
+};
+export const pathToNameFormat = (str: string) => {
+ if (!str.startsWith('/')) return str.replace(/(\/|\.)/g, '-').toLocaleLowerCase();
+ return str
+ .substring(1)
+ .replace(/(\/|\.)/g, '-')
+ .toLocaleLowerCase();
+};
+
+/**
+ * read a file text content
+ */
+export const reactLocalFileContent = (file: File) => {
+ return new Promise((resolve: (_: string) => void, reject) => {
+ const reader = new FileReader();
+ reader.onload = () => {
+ resolve(reader.result as string);
+ };
+ reader.onerror = (err) => {
+ reject(err);
+ };
+ reader.readAsText(file);
+ });
+};
+
+/**
+ * str to base64
+ */
+export const strToBase64 = (str: string) => {
+ try {
+ const base64 = window.btoa(str);
+ return base64;
+ } catch (error) {
+ console.log(error);
+ }
+ return '';
+};
+
+/**
+ * cpu format
+ */
+export const cpuFormatToM = (cpu = '0') => {
+ if (!cpu || cpu === '0') {
+ return 0;
+ }
+ let value = parseFloat(cpu);
+
+ if (/n/gi.test(cpu)) {
+ value = value / 1000 / 1000;
+ } else if (/u/gi.test(cpu)) {
+ value = value / 1000;
+ } else if (/m/gi.test(cpu)) {
+ value = value;
+ } else {
+ value = value * 1000;
+ }
+ if (value < 0.1) return 0;
+ return Number(value.toFixed(4));
+};
+
+/**
+ * memory format
+ */
+export const memoryFormatToMi = (memory = '0') => {
+ if (!memory || memory === '0') {
+ return 0;
+ }
+
+ let value = parseFloat(memory);
+
+ if (/Ki/gi.test(memory)) {
+ value = value / 1024;
+ } else if (/Mi/gi.test(memory)) {
+ value = value;
+ } else if (/Gi/gi.test(memory)) {
+ value = value * 1024;
+ } else if (/Ti/gi.test(memory)) {
+ value = value * 1024 * 1024;
+ } else {
+ console.log('Invalid memory value');
+ value = 0;
+ }
+
+ return Number(value.toFixed(2));
+};
+
+/**
+ * storage format
+ */
+export const storageFormatToNum = (storage = '0') => {
+ return +`${storage.replace(/gi/i, '')}`;
+};
+
+/**
+ * print memory to Mi of Gi
+ */
+export const printMemory = (val: number) => {
+ return val >= 1024 ? `${Math.round(val / 1024)} Gi` : `${val} Mi`;
+};
+
+/**
+ * format pod createTime
+ */
+export const formatPodTime = (createTimeStamp: Date = new Date()) => {
+ const podStartTimeStamp = dayjs(createTimeStamp);
+
+ let timeDiff = Math.floor(dayjs().diff(podStartTimeStamp) / 1000);
+
+ const days = Math.floor(timeDiff / (24 * 60 * 60));
+ timeDiff -= days * 24 * 60 * 60;
+
+ const hours = Math.floor(timeDiff / (60 * 60));
+ timeDiff -= hours * 60 * 60;
+
+ const minutes = Math.floor(timeDiff / 60);
+ timeDiff -= minutes * 60;
+
+ const seconds = timeDiff;
+
+ if (days > 0) {
+ return `${days}d${hours}h`;
+ }
+ if (hours > 0) {
+ return `${hours}h${minutes}m`;
+ }
+ if (minutes > 0) {
+ return `${minutes}m${seconds}s`;
+ }
+ return `${seconds}s`;
+};
+
+export function downLoadBold(content: BlobPart, type: string, fileName: string) {
+ const blob = new Blob([content], { type });
+
+ const url = URL.createObjectURL(blob);
+
+ const link = document.createElement('a');
+ link.href = url;
+ link.download = fileName;
+
+ link.click();
+}
+
+export const getErrText = (err: any, def = '') => {
+ const msg = typeof err === 'string' ? err : err?.message || def || '';
+ msg && console.log('error =>', msg);
+ return msg;
+};
+
+export const delay = (ms: number) =>
+ new Promise((resolve) => {
+ setTimeout(() => {
+ resolve('');
+ }, ms);
+ });
+
+export const convertCronTime = (cronTime: string, offset: 8 | -8) => {
+ let [minute, hour, dayOfMonth, month, dayOfWeek] = cronTime.split(' ');
+
+ if (hour === '*') return cronTime;
+
+ const cronDate = set(startOfDay(new Date()), { hours: +hour, minutes: +minute });
+
+ const newCronDate = addHours(cronDate, offset);
+
+ minute = format(newCronDate, 'mm');
+ hour = format(newCronDate, 'HH');
+
+ const daysOfWeek =
+ dayOfWeek === '*'
+ ? [dayOfWeek]
+ : dayOfWeek.split(',').map((day) => {
+ if (offset < 0) {
+ const newDay = +day + (+hour >= 16 ? -1 : 0);
+ return newDay === -1 ? '7' : String(newDay);
+ }
+ const newDay = +day + (+hour < 8 ? 1 : 0);
+ return newDay === 7 ? '0' : String(newDay);
+ });
+
+ return `${minute} ${hour} ${dayOfMonth} ${month} ${daysOfWeek.join(',')}`;
+};
+
+// convertBytes 1024
+export const convertBytes = (bytes: number, unit: 'kb' | 'mb' | 'gb' | 'tb') => {
+ switch (unit.toLowerCase()) {
+ case 'kb':
+ return bytes / 1024;
+ case 'mb':
+ return bytes / Math.pow(1024, 2);
+ case 'gb':
+ return bytes / Math.pow(1024, 3);
+ case 'tb':
+ return bytes / Math.pow(1024, 4);
+ default:
+ return bytes;
+ }
+};
+
+// formatTime second to day, hour or minute
+export const formatTimeToDay = (seconds: number): { time: string; unit: string } => {
+ const minutes = Math.floor(seconds / 60);
+ const hours = Math.floor(seconds / 3600);
+ const days = Math.floor(seconds / (3600 * 24));
+
+ if (days > 0) {
+ return {
+ unit: 'Day',
+ time: (seconds / (3600 * 24)).toFixed(1)
+ };
+ } else if (hours > 0) {
+ return {
+ unit: 'Hour',
+ time: (seconds / 3600).toFixed(1)
+ };
+ } else {
+ return {
+ unit: 'Start Minute',
+ time: (seconds / 60).toFixed(1)
+ };
+ }
+};
+
+export const obj2Query = (obj: Record) => {
+ let str = '';
+ Object.entries(obj).forEach(([key, val]) => {
+ if (val) {
+ str += `${key}=${val}&`;
+ }
+ });
+
+ return str.slice(0, str.length - 1);
+};
diff --git a/frontend/providers/workorder/tsconfig.json b/frontend/providers/workorder/tsconfig.json
new file mode 100644
index 000000000..0eef8192c
--- /dev/null
+++ b/frontend/providers/workorder/tsconfig.json
@@ -0,0 +1,21 @@
+{
+ "compilerOptions": {
+ "lib": [
+ "dom",
+ "dom.iterable",
+ "esnext"
+ ],
+ "baseUrl": ".",
+ "paths": {
+ "@/*": [
+ "./src/*"
+ ]
+ }
+ },
+ "include": [
+ "next-env.d.ts",
+ "src/**/*.ts",
+ "src/**/*.tsx"
+ ],
+ "extends": "../../tsconfig.web.json"
+}
\ No newline at end of file