فهرست منبع

Merge branch 'develop'

xujunjie 2 سال پیش
والد
کامیت
5253518c61
100فایلهای تغییر یافته به همراه9517 افزوده شده و 837 حذف شده
  1. 95 12
      .umirc.ts
  2. 22 4
      src/app.ts
  3. BIN
      src/assets/center/icon-arr.png
  4. BIN
      src/assets/center/my-task.png
  5. BIN
      src/assets/center/my-task@2x.png
  6. BIN
      src/assets/center/order.png
  7. BIN
      src/assets/center/order@2x.png
  8. BIN
      src/assets/center/profile.png
  9. BIN
      src/assets/current.png
  10. BIN
      src/assets/smartOps/icon01.png
  11. BIN
      src/assets/smartOps/icon02.png
  12. BIN
      src/assets/smartOps/icon03.png
  13. BIN
      src/assets/smartOps/icon04.png
  14. BIN
      src/assets/smartOps/icon05.png
  15. BIN
      src/assets/smartOps/icon06.png
  16. BIN
      src/assets/smartOps/icon07.png
  17. BIN
      src/assets/smartOps/icon08.png
  18. BIN
      src/assets/tbodyBg1.png
  19. 33 12
      src/components/PageContent/index.js
  20. 8 0
      src/components/PageContent/index.less
  21. 4 2
      src/components/PageTitle/index.js
  22. 2 0
      src/components/PageTitle/index.less
  23. 33 0
      src/components/ScrollLoading/index.js
  24. 0 0
      src/components/ScrollLoading/index.less
  25. 2 2
      src/components/TabsContent/index.less
  26. 1 0
      src/constants/index.ts
  27. 61 2
      src/global.less
  28. 19 3
      src/models/eqSelfInspection.js
  29. 376 0
      src/pages/Center/MyTask/Detail/TaskDetail.js
  30. 440 0
      src/pages/Center/MyTask/Detail/WorkOrderDetail.js
  31. 125 0
      src/pages/Center/MyTask/Detail/taskDetail.less
  32. 47 0
      src/pages/Center/MyTask/Detail/workOrderDetail.less
  33. 308 0
      src/pages/Center/MyTask/List/TaskList.js
  34. 168 0
      src/pages/Center/MyTask/List/WorkOrderList.js
  35. 29 0
      src/pages/Center/MyTask/List/WorkOrderList.less
  36. 91 0
      src/pages/Center/MyTask/List/taskList.less
  37. 178 0
      src/pages/Center/MyTask/index.js
  38. 79 0
      src/pages/Center/MyTask/index.less
  39. 85 0
      src/pages/Center/index.js
  40. 104 0
      src/pages/Center/index.less
  41. 6 0
      src/pages/Controller/Device.js
  42. 1 1
      src/pages/Controller/index.js
  43. 174 43
      src/pages/DeviceManager/detail.js
  44. 38 30
      src/pages/DeviceManager/index.js
  45. 62 60
      src/pages/DeviceManager/storage.js
  46. 1 2
      src/pages/EqSelfInspection/List/index.js
  47. 1 1
      src/pages/EqSelfInspection/ReportDetail.js
  48. 195 0
      src/pages/EqSelfInspection/components/Detail.js
  49. 142 96
      src/pages/EqSelfInspection/index.js
  50. 9 3
      src/pages/EqSelfInspection/index.less
  51. 0 0
      src/pages/Home/backlog.js
  52. 62 5
      src/pages/Home/index.js
  53. 58 5
      src/pages/Home/index.less
  54. 14 11
      src/pages/Menu/index.js
  55. 1 1
      src/pages/MessageCenter/index.less
  56. 14 46
      src/pages/SafetyManagement/doorDetail.js
  57. 69 61
      src/pages/SafetyManagement/index.js
  58. 66 64
      src/pages/Smart/ConditionDetection.js
  59. 62 67
      src/pages/Smart/OptimizationTasks.js
  60. 29 2
      src/pages/Smart/OptimizationTasks.less
  61. 2 2
      src/pages/Smart/components/SimulatePie.js
  62. 4 2
      src/pages/Smart/index.js
  63. 0 1
      src/pages/Smart/index.less
  64. 204 0
      src/pages/SmartOps/Analysis.js
  65. 216 0
      src/pages/SmartOps/ChartPage.js
  66. 205 0
      src/pages/SmartOps/HistoryRecord.js
  67. 235 0
      src/pages/SmartOps/OperationRecord.js
  68. 839 0
      src/pages/SmartOps/WorkAnalysisDetail.js
  69. 92 0
      src/pages/SmartOps/WorkAnalysisDetail.less
  70. 61 0
      src/pages/SmartOps/components/BarModal.js
  71. 28 0
      src/pages/SmartOps/components/BarModal.less
  72. 179 0
      src/pages/SmartOps/components/DeviceAnalysis.js
  73. 11 0
      src/pages/SmartOps/components/DeviceAnalysis.less
  74. 52 0
      src/pages/SmartOps/components/ThresholdBar.js
  75. 46 0
      src/pages/SmartOps/components/ThresholdBar.less
  76. 503 0
      src/pages/SmartOps/components/VideoAnalysis.js
  77. 111 0
      src/pages/SmartOps/components/VideoAnalysis.less
  78. 50 0
      src/pages/SmartOps/components/WorkAnalysis.js
  79. 43 0
      src/pages/SmartOps/components/WorkAnalysis.less
  80. 373 0
      src/pages/SmartOps/index.js
  81. 180 0
      src/pages/SmartOps/index.less
  82. 87 0
      src/pages/SmartOps/models/smartOps.js
  83. 0 0
      src/pages/SmartReport/components/EqSelfPie.js
  84. 465 0
      src/pages/SmartReport/index.js
  85. 96 0
      src/pages/SmartReport/index.less
  86. 87 0
      src/pages/SystemDaily/index.js
  87. 38 0
      src/pages/SystemDaily/index.less
  88. 113 111
      src/pages/TaskManage/Detail/TaskDetail/TaskDetail.tsx
  89. 2 1
      src/pages/TaskManage/Detail/TaskDetail/taskDetail.types.ts
  90. 21 59
      src/pages/TaskManage/Detail/TaskList/TaskList.tsx
  91. 22 10
      src/pages/TaskManage/Detail/TaskList/taskList.less
  92. 196 116
      src/pages/TaskManage/Detail/TaskOrder/TaskOrder.tsx
  93. 282 0
      src/pages/TaskManage/Detail/WorkOrderList/WorkOrderList.js
  94. 36 0
      src/pages/TaskManage/Detail/WorkOrderList/WorkOrderList.less
  95. 394 0
      src/pages/TaskManage/Popup/WorkOrderModal.js
  96. 46 0
      src/pages/TaskManage/Popup/WorkOrderModal.less
  97. 158 0
      src/pages/TaskManage/Popup/index.js
  98. 43 0
      src/pages/TaskManage/Popup/index.less
  99. 589 0
      src/pages/TaskManage/components/MandateDetail.js
  100. 94 0
      src/pages/TaskManage/components/MandateDetail.less

+ 95 - 12
.umirc.ts

@@ -18,12 +18,17 @@ export default defineConfig({
     { 'http-equiv': 'expires', content: '0' },
     { 'http-equiv': 'X-UA-Compatible', content: 'IE=EmulateIE9' },
     { name: 'transparent', content: 'true' },
-    { name: 'viewport', content: 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0' },
+    {
+      name: 'viewport',
+      content:
+        'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0',
+    },
   ],
   proxy: {
     '/api': {
       // target: 'http://47.96.12.136:8888/',
       target: 'http://47.96.12.136:8788/',
+      // target: 'http://120.55.44.4:8903/',
       // target: 'https://work.greentech.com.cn/',
       changeOrigin: true,
     },
@@ -129,7 +134,7 @@ export default defineConfig({
       component: './EqSelfInspection/Statistics',
     },
     {
-      name: '任务管理',
+      name: '任务管理 - 工单管理',
       path: '/task-manage/:projectID',
       component: './TaskManage',
     },
@@ -143,11 +148,26 @@ export default defineConfig({
       path: '/task-manage/list/detail',
       component: './TaskManage/Detail/TaskDetail/TaskDetail',
     },
+    {
+      name: '工单列表',
+      path: '/task-manage/work-order/list',
+      component: './TaskManage/Detail/WorkOrderList/WorkOrderList',
+    },
     {
       name: '任务关联工单详情',
       path: '/task-manage/list/order-detail',
       component: './TaskManage/Detail/TaskOrder/TaskOrder',
     },
+    {
+      name: '任务弹窗',
+      path: '/task-manage/pop/:projectID',
+      component: './TaskManage/Popup',
+    },
+    {
+      name: '工单详情弹窗',
+      path: '/task-manage/pop/work-order/:projectID',
+      component: './TaskManage/Popup/WorkOrderModal',
+    },
     {
       name: '消息中心',
       path: '/message/:projectId',
@@ -178,16 +198,79 @@ export default defineConfig({
       path: '/safety/detail/:projectId', ///safety management
       component: './SafetyManagement/doorDetail',
     },
-    // {
-    //   name: '权限演示',
-    //   path: '/access',
-    //   component: './Access',
-    // },
-    // {
-    //   name: ' CRUD 示例',
-    //   path: '/table',
-    //   component: './Table',
-    // },
+    // 智慧运营
+    {
+      name: '',
+      path: '/smart-ops/:projectId', ///safety management
+      component: './SmartOps/index',
+    },
+    // 工况分析详情
+    {
+      name: '',
+      path: '/smart-ops/work-analysis-detail/:projectId', ///safety management
+      component: './SmartOps/WorkAnalysisDetail',
+    },
+    // 历史记录
+    {
+      name: '',
+      path: '/smart-ops/history-record/:projectId', ///safety management
+      component: './SmartOps/HistoryRecord',
+    },
+    // 操作记录
+    {
+      name: '',
+      path: '/smart-ops/operation-record/:projectId',
+      component: './SmartOps/OperationRecord',
+    },
+    // 图标弹窗页面
+    {
+      name: '',
+      path: '/smart-ops/chart-page/:projectId',
+      component: './SmartOps/ChartPage',
+    },
+    // 系统工作日报
+    {
+      name: '',
+      path: '/system-daily/:projectId',
+      component: './SystemDaily/index',
+    },
+    //个人中心
+    {
+      name: '个人中心',
+      path: '/center/:projectId',
+      component: './Center/index',
+    },
+    {
+      name: '我的任务',
+      path: '/center/my-task/:projectId',
+      component: './Center/MyTask',
+    },
+    {
+      name: '我的任务列表',
+      path: '/center/my-task/task-list',
+      component: './Center/MyTask/List/TaskList',
+    },
+    {
+      name: '我的工单列表',
+      path: '/center/my-task/work-order-list',
+      component: './Center/MyTask/List/WorkOrderList',
+    },
+    {
+      name: '我的任务详情',
+      path: '/center/my-task/task-detail',
+      component: './Center/MyTask/Detail/TaskDetail',
+    },
+    {
+      name: '我的工单详情',
+      path: '/center/my-task/work-order-detail',
+      component: './Center/MyTask/Detail/WorkOrderDetail',
+    },
+    //智慧运营报告
+    {
+      name: '',
+      path: '/smart-report/:projectId',
+      component: './SmartReport/index',
+    },
   ],
   npmClient: 'yarn',
 });

+ 22 - 4
src/app.ts

@@ -2,15 +2,33 @@
 
 // import { RequestConfig, history } from '@umijs/max';
 import { getToken, GetTokenFromUrl } from '@/utils/utils';
-import { history, RequestConfig } from '@umijs/max';
+import { RequestConfig } from '@umijs/max';
 import { message } from 'antd';
+import dayjs from 'dayjs';
+import 'dayjs/locale/zh-cn';
+import { queryCurrentV2 } from './services/user';
+
+dayjs.locale('zh-cn');
 
 // 全局初始化数据配置,用于 Layout 用户信息和权限初始化
 // 更多信息见文档:https://umijs.org/docs/api/runtime-config#getinitialstate
-export async function getInitialState(): Promise<{ name: string }> {
-  return { name: '@umijs/max' };
-}
+export async function getInitialState(): Promise<any> {
+  const { data: user } = await queryCurrentV2();
 
+  let permission = {};
+  user.Permissions?.forEach((item: any) => {
+    permission = {
+      ...permission,
+      ...item.Menus,
+    };
+  });
+  return {
+    user: {
+      ...user,
+      Permission: permission,
+    },
+  };
+}
 // axios配置
 export const request: RequestConfig = {
   errorConfig: {

BIN
src/assets/center/icon-arr.png


BIN
src/assets/center/my-task.png


BIN
src/assets/center/my-task@2x.png


BIN
src/assets/center/order.png


BIN
src/assets/center/order@2x.png


BIN
src/assets/center/profile.png


BIN
src/assets/current.png


BIN
src/assets/smartOps/icon01.png


BIN
src/assets/smartOps/icon02.png


BIN
src/assets/smartOps/icon03.png


BIN
src/assets/smartOps/icon04.png


BIN
src/assets/smartOps/icon05.png


BIN
src/assets/smartOps/icon06.png


BIN
src/assets/smartOps/icon07.png


BIN
src/assets/smartOps/icon08.png


BIN
src/assets/tbodyBg1.png


+ 33 - 12
src/components/PageContent/index.js

@@ -1,25 +1,46 @@
 import { UnityAction } from '@/utils/utils';
-import { CloseOutlined } from '@ant-design/icons';
+import { CloseOutlined, LeftOutlined } from '@ant-design/icons';
+import { history } from '@umijs/max';
+import { ConfigProvider } from 'antd';
+import locale from 'antd/es/locale/zh_CN';
 import styles from './index.less';
 
 export default (props) => {
-  const { children, style, closeable = true, tabs = false } = props;
+  const {
+    children,
+    style,
+    closeable = true,
+    returnable = false,
+    tabs = false,
+  } = props;
 
   const handleClose = () => {
     UnityAction.sendMsg('closePage');
   };
+  const handleReturn = () => {
+    history.back();
+  };
 
   return (
-    <div className={styles.page} style={style}>
-      {closeable && (
-        <CloseOutlined
-          onClick={handleClose}
-          className={styles.close}
-          style={{ top: tabs ? 65 : '' }}
-        />
-      )}
+    <ConfigProvider locale={locale}>
+      <div className={styles.page} style={style}>
+        {returnable && (
+          <LeftOutlined
+            onClick={handleReturn}
+            className={styles.return}
+            style={{ top: tabs ? 42.5 : '' }}
+          />
+        )}
+        {closeable && (
+          <CloseOutlined
+            onClick={handleClose}
+            className={styles.close}
+            style={{ top: tabs ? 42.5 : '' }}
+          />
+        )}
 
-      {children}
-    </div>
+        {children}
+      </div>
+    </ConfigProvider>
   );
 };

+ 8 - 0
src/components/PageContent/index.less

@@ -13,4 +13,12 @@
     color: #4a4a4a;
     cursor: pointer;
   }
+  .return {
+    position: absolute;
+    top: 26px;
+    left: 20px;
+    font-size: 34px;
+    color: #4a4a4a;
+    cursor: pointer;
+  }
 }

+ 4 - 2
src/components/PageTitle/index.js

@@ -14,10 +14,12 @@ export default (props) => {
   };
 
   return (
-    <div className={`${styles.titleBox} ${tabs ? styles.tabs : ''}`}>
+    <div
+      className={`${styles.titleBox} ${tabs ? styles.tabs : ''}`}
+      onClick={handleOnClick}
+    >
       {returnable || onReturn ? (
         <LeftOutlined
-          onClick={handleOnClick}
           style={{ fontSize: 36, cursor: 'pointer', marginRight: '20px' }}
         />
       ) : (

+ 2 - 0
src/components/PageTitle/index.less

@@ -10,6 +10,8 @@
   margin-right: 18px;
 }
 .title {
+  display: inline-block;
+  width: 100%;
   font-size: 36px;
   font-weight: 400;
   color: #000000;

+ 33 - 0
src/components/ScrollLoading/index.js

@@ -0,0 +1,33 @@
+import { Spin } from 'antd';
+import { useRef } from 'react';
+
+export default function ScrollLoading({
+  loading,
+  children,
+  pagination = { current: 1, total: 0, pageSize: 20 },
+  handleLoadData, //请求数据方法
+  height = 90, //除滚动区域外其他高度(只是通用标题就是90)
+}) {
+  const scrollContent = useRef();
+
+  const handleScroll = () => {
+    const { current, total, pageSize } = pagination;
+    if (current * pageSize >= total) return;
+    const { scrollHeight, clientHeight, scrollTop } = scrollContent.current;
+    if (scrollHeight - clientHeight <= scrollTop + 1 && !loading) {
+      handleLoadData(current + 1);
+    }
+  };
+
+  return (
+    <Spin spinning={loading}>
+      <div
+        ref={scrollContent}
+        onScroll={handleScroll}
+        style={{ overflowY: 'scroll', height: `calc(100vh - ${height}px)` }}
+      >
+        {children}
+      </div>
+    </Spin>
+  );
+}

+ 0 - 0
src/components/ScrollLoading/index.less


+ 2 - 2
src/components/TabsContent/index.less

@@ -1,7 +1,7 @@
 .tabsTitle {
   display: flex;
   align-items: center;
-  height: 116px;
+  height: 80px;
   margin-bottom: 10px;
   width: 100%;
   overflow-x: auto;
@@ -32,7 +32,7 @@
   padding: 20px 0;
   .tabsItem {
     font-size: 28px;
-    padding: 0 30px; 
+    padding: 0 30px;
     &:last-child {
       padding-right: 0;
     }

+ 1 - 0
src/constants/index.ts

@@ -1 +1,2 @@
 export const DEFAULT_NAME = 'Umi Max';
+export const version = 'v1.0.47';

+ 61 - 2
src/global.less

@@ -57,6 +57,23 @@ body {
     border-bottom: none !important;
     margin-bottom: 10px;
   }
+  .ant-select-dropdown .ant-select-item {
+    font-size: 26px;
+  }
+  .ant-picker-dropdown {
+    font-size: 24px;
+  }
+  .ant-picker-dropdown .ant-picker-content td {
+    width: 30px;
+  }
+  .ant-picker-dropdown .ant-picker-cell::before {
+    height: 30px;
+  }
+  .ant-picker-dropdown .ant-picker-cell .ant-picker-cell-inner {
+    min-width: 30px;
+    height: 30px;
+    line-height: 30px;
+  }
 }
 
 // Remove list styles on ul, ol
@@ -113,6 +130,22 @@ input[type='reset'] {
   background: rgba(255, 255, 255);
 }
 
+.content-tab {
+  padding-left: 5px;
+  padding-top: 5px;
+  padding-right: 5px;
+  height: calc(100vh - 130px);
+  overflow-x: hidden;
+  overflow-y: auto;
+}
+.content-title {
+  padding-left: 5px;
+  padding-right: 5px;
+  height: calc(100vh - 110px);
+  overflow-x: hidden;
+  overflow-y: auto;
+}
+
 .password-eye {
   display: inline-block;
   vertical-align: middle;
@@ -188,8 +221,8 @@ input[type='reset'] {
   padding-top: 0;
   padding-bottom: 0;
 }
-.ant-table{
-  background-color: white!important;
+.ant-table {
+  background-color: white !important;
 }
 .ant-table-cell,
 .ant-table-placeholder {
@@ -224,3 +257,29 @@ input[type='reset'] {
 .ant-table-tbody > tr:nth-child(even) {
   background: rgba(145, 192, 238, 0.16);
 }
+
+.ant-picker {
+  font-size: 26px;
+  .ant-picker-input > input {
+    font-size: 24px;
+  }
+  .ant-picker-separator {
+    font-size: 24px;
+  }
+}
+
+.ant-select-selection-item {
+  font-size: 26px;
+}
+.ant-select-selector {
+  height: 40px;
+  padding: 4px 11px;
+}
+
+.ant-select-single:not(.ant-select-customize-input) .ant-select-selector {
+  height: 40px;
+  padding: 4px 11px;
+}
+.ant-select-single .ant-select-selector {
+  font-size: 26px;
+}

+ 19 - 3
src/models/eqSelfInspection.js

@@ -194,20 +194,36 @@ export default {
         data.extendWarningAllData = arr;
         data.extendWarningData =
           arr?.filter((item) => item.Status === 2 || item.Status === 1) || [];
-        data.extendWarningNum = arr?.filter((item) => item.Status === 1)?.length || 0;
+        data.extendWarningNum =
+          arr?.filter((item) => item.Status === 1)?.length || 0;
 
+        //获取液位检测异常数量
+        const errorNum = data.FluidLevelList?.filter(
+          (item) => item.status == 1,
+        );
         let num = 0;
         if (data?.extendWarningData?.length > 0) num += 1;
         if (data?.sensorWaringNum > 0) num += 1;
         if (data?.dumuList?.length > 0) num += 1;
         if (data?.FaultAnalysis?.length > 0) num += 1;
+        if (errorNum?.length > 0) num += errorNum;
         data.warningTotalNum = num;
 
-        data.patrolStatus = data?.extendWarningNum?.length > 0 ? 1 : 0;
+        if (data.extendWarningNum > 0) {
+          // 判断是否有异常
+          data.patrolStatus = 1;
+        } else if (data.extendWarningData.length - data.extendWarningNum > 0) {
+          // extendWarningData包含警告和异常
+          // 判断是否有警告
+          data.patrolStatus = 2;
+        } else {
+          data.patrolStatus = 0;
+        }
         data.faultAnalysisStatus = data?.FaultAnalysis?.length > 0 ? 1 : 0;
         // data.secureStatus =
         //   data?.dumuList?.length > 0 || data?.sensorWaringNum > 0 ? 1 : 0;
-        data.secureStatus = data?.dumuList?.length > 0 ? 1 : 0;
+        data.secureStatus =
+          data?.dumuList?.length > 0 || errorNum?.length > 0 ? 1 : 0;
         let secureChild = [];
         let dumuStatus = 0;
         if (data?.dumuList?.length > 0) {

+ 376 - 0
src/pages/Center/MyTask/Detail/TaskDetail.js

@@ -0,0 +1,376 @@
+import PageContent from '@/components/PageContent';
+import PageTitle from '@/components/PageTitle';
+import {
+  MandateClass,
+  MandateStatus,
+  MandateType,
+  OrderStatus,
+  OrderType,
+} from '@/pages/TaskManage/constent';
+import { getDiagnosticDetail, getMandateDetail } from '@/services/TaskManage';
+import { useLocation } from '@@/exports';
+import { UpOutlined } from '@ant-design/icons';
+import { connect, useRequest } from '@umijs/max';
+import { Col, Collapse, Divider, Row, Table } from 'antd';
+import dayjs from 'dayjs';
+import { useEffect, useState } from 'react';
+// @ts-ignore
+import ReactZmage from 'react-zmage';
+import { useNavigate } from 'umi';
+import styles from './taskDetail.less';
+
+function TaskDetail(props) {
+  const { userList, dispatch } = props;
+
+  const location = useLocation();
+  const queryParams = new URLSearchParams(location.search);
+  const project_id = Number(queryParams.get('project_id'));
+  const mandate_id = Number(queryParams.get('mandate_id'));
+
+  const navigate = useNavigate();
+
+  const [mandateDetail, setMandateDetail] = useState();
+  const [mandateChild, setMandateChild] = useState([]);
+  const [handledWorkOrder, setHandledWorkOrder] = useState([]);
+  const [mandateTable, setMandateTable] = useState([]);
+
+  const columnDef = [
+    {
+      title: '详情',
+      dataIndex: 'detail',
+      key: 'key',
+      render: (value, record) => {
+        return (
+          <div style={{ display: 'flex', alignItems: 'center' }}>
+            <div style={{ width: '100%' }}>{value.text}</div>
+          </div>
+        );
+      },
+    },
+  ];
+
+  const base64ToImageUrl = (base64String) => {
+    const byteCharacters = atob(base64String);
+    const byteArrays = [];
+
+    for (let i = 0; i < byteCharacters.length; i++) {
+      byteArrays.push(byteCharacters.charCodeAt(i));
+    }
+
+    const byteArray = new Uint8Array(byteArrays);
+    const blob = new Blob([byteArray], { type: 'image/png' });
+    return URL.createObjectURL(blob);
+  };
+
+  const { refresh: refreshDetail } = useRequest(getMandateDetail, {
+    defaultParams: [
+      {
+        mandate_id,
+        project_id,
+      },
+    ],
+    formatResult: async (result) => {
+      const tempMandate = {
+        ...result.data,
+        Status: MandateStatus.find((item) => item.value === result.data.Status),
+        MandateClass: MandateClass.find(
+          (item) => item.value === result.data.MandateClass,
+        ),
+        MandateType: MandateType.find(
+          (item) => item.value === result.data.MandateType,
+        ),
+        ResponsiblePeople: userList.find(
+          (item) => item.ID === result.data.ResponsiblePeople,
+        ),
+        CreateTime: dayjs(result.data.CreateTime).format('YYYY-MM-DD HH:mm'),
+      };
+      const workOrder = result.data.Records.map((item) => {
+        return {
+          ...item,
+          CreateTime: dayjs(item.CreateTime).format('YYYY-MM-DD HH:mm'),
+          Status: OrderStatus.find((status) => status.value === item.Status),
+          RecordType: OrderType.find((type) => type.value === item.RecordType),
+          Responsible: userList.find((user) => user.ID === item.Responsible),
+        };
+      });
+      const children = result.data.MandateChild;
+
+      const tempOrder = [
+        {
+          key: '1',
+          label: (
+            <span style={{ color: '#5697e4' }}>
+              关联工单({workOrder.length})
+            </span>
+          ),
+          children: workOrder.map((record) => {
+            return (
+              <div key={record.Id} className={styles.workOrderCard}>
+                <div className={styles.leftInfo}>
+                  <Row style={{ marginBottom: '15px' }}>
+                    <Col className={styles.fontS24} span={12}>
+                      <>
+                        工单类型:
+                        {record.RecordType?.label?.replace('工单', '')}
+                      </>
+                    </Col>
+                    <Col className={styles.fontS24} span={12}>
+                      时间:{record.CreateTime || '-'}
+                    </Col>
+                  </Row>
+                  <Row>
+                    <Col className={styles.fontS24} span={12}>
+                      工单状态:
+                      <span style={{ color: '#5697e4' }}>
+                        {typeof record.Status === 'number'
+                          ? '-'
+                          : record.Status?.label}
+                      </span>
+                    </Col>
+                    <Col className={styles.fontS24} span={12}>
+                      工单负责人:
+                      {typeof record.Responsible === 'number'
+                        ? '-'
+                        : record.Responsible?.CName}
+                    </Col>
+                  </Row>
+                </div>
+                <Divider type="vertical" style={{ height: '40px' }} />
+                <div
+                  className={styles.rightButton}
+                  style={{ color: '#5697e4' }}
+                  onClick={() => {
+                    if (typeof record.RecordType === 'number') {
+                      return;
+                    }
+                    // @ts-ignore
+                    goTaskOrder(
+                      record.Id,
+                      record.RecordType?.value,
+                      tempMandate?.MandateClass.value,
+                    );
+                  }}
+                >
+                  查看工单
+                </div>
+              </div>
+            );
+          }),
+        },
+      ];
+
+      if (
+        tempMandate.MandateClass &&
+        tempMandate.ExtendId &&
+        /* @ts-ignore */
+        tempMandate.MandateClass.value === 7
+      ) {
+        const image = await getDiagnosticDetail(tempMandate.ExtendId);
+        tempMandate.img = image.path;
+      }
+      setMandateDetail(tempMandate);
+      setHandledWorkOrder(tempOrder);
+      if (children && children.length) {
+        setMandateChild(children);
+      }
+    },
+  });
+
+  useEffect(() => {
+    if (userList.length === 0) {
+      dispatch({
+        type: 'taskUser/fetchUserList',
+        payload: { project_id },
+      });
+    }
+  }, []);
+
+  useEffect(() => {
+    if (!mandateChild.length) {
+      return;
+    }
+
+    if (mandateDetail?.MandateClass?.value === 2) {
+      const dataSource = [];
+      dataSource.push({
+        detail: {
+          text: mandateChild[0].Title,
+          key: 'title',
+        },
+      });
+      dataSource.push(
+        ...Object.entries(JSON.parse(mandateChild[0].Payload)).map((item) => {
+          const [key, value] = item;
+          return {
+            detail: {
+              text:
+                value['item_alias'] +
+                ' 现有数值:' +
+                value['old_value'] +
+                ' 建议调整数值' +
+                value['new_value'],
+              key: key,
+            },
+          };
+        }),
+      );
+      setMandateTable(dataSource);
+      return;
+    }
+
+    const dataSource = mandateChild.map((item, index) => {
+      if (item.MandateClass === 2) {
+      }
+
+      return {
+        detail: {
+          text: item.Title + item.Content,
+          key: item.Title + index + item.Content,
+        },
+      };
+    });
+    setMandateTable(dataSource);
+  }, [mandateChild]);
+
+  const goTaskOrder = (orderID, orderType, mandateClass) => {
+    navigate(
+      `/task-manage/list/order-detail?project_id=${project_id}&order_id=${orderID}&order_type=${orderType}&mandate_class=${mandateClass}`,
+    );
+  };
+
+  return (
+    <PageContent closeable={false}>
+      <PageTitle returnable>任务详情</PageTitle>
+      <div className={`${styles.cardContainer} card-box`}>
+        <div className={styles.normalInfo}>
+          <Row className={styles.infoRow} justify="space-between">
+            <Col className={styles.fontS24}>
+              时间:{mandateDetail?.CreateTime}
+            </Col>
+            {/*// @ts-ignore*/}
+            <Col className={styles.fontS24}>
+              {/*//@ts-ignore*/}
+              任务类别:{mandateDetail?.MandateClass?.label}
+            </Col>
+          </Row>
+          <Row justify="space-between">
+            <Col className={styles.fontS24}>
+              {/*//@ts-ignore*/}
+              任务状态:{mandateDetail?.Status?.label}
+            </Col>
+            <Col className={styles.fontS24}>
+              {/*// @ts-ignore*/}
+              任务负责人:{mandateDetail?.ResponsiblePeople?.CName}
+            </Col>
+          </Row>
+        </div>
+        <div className={styles.detailInfo}>
+          <Row className={styles.infoRow}>
+            <Col className={styles.fontS24} span={4}>
+              任务总结
+            </Col>
+            <Col className={styles.fontS24}>
+              {mandateDetail?.Summary ||
+                '根据水质相关数据.建议您调节以下参数,水厂运行可达较优状态'}
+            </Col>
+          </Row>
+          {mandateDetail?.img && (
+            <Row className={styles.infoRow}>
+              <Col className={styles.fontS24} span={4}>
+                预警图片
+              </Col>
+              <Col className={styles.fontS24}>
+                <ReactZmage
+                  controller={{
+                    // 关闭按钮
+                    close: true,
+                    // 缩放按钮
+                    zoom: false,
+                    // 下载按钮
+                    download: false,
+                    // 翻页按钮
+                    flip: false,
+                    // 多页指示
+                    pagination: false,
+                  }}
+                  backdrop="rgba(255,255,255,0.5)"
+                  style={{ width: '350px' }}
+                  src={mandateDetail?.img}
+                />
+              </Col>
+            </Row>
+          )}
+
+          {mandateDetail?.Files.length > 0 && (
+            <Row className={styles.infoRow}>
+              <Col className={styles.fontS24} span={4}>
+                截图
+              </Col>
+              <Col className={styles.fontS24}>
+                <ReactZmage
+                  controller={{
+                    // 关闭按钮
+                    close: true,
+                    // 缩放按钮
+                    zoom: false,
+                    // 下载按钮
+                    download: false,
+                    // 翻页按钮
+                    flip: true,
+                    // 多页指示
+                    pagination: true,
+                  }}
+                  backdrop="rgba(255,255,255,0.5)"
+                  style={{ width: '350px' }}
+                  src={mandateDetail?.Files[0].url}
+                  set={mandateDetail?.Files.map((item) => {
+                    if (item) {
+                      return {
+                        src: item.url,
+                      };
+                    }
+                    return {};
+                  })}
+                />
+              </Col>
+            </Row>
+          )}
+
+          <Row>
+            <Col className={styles.fontS24} span={4}>
+              任务内容
+            </Col>
+            <Col className={styles.fontS24} span={20}>
+              {/*{mandateDetail?.Detail}*/}
+              <Table
+                rowKey="key"
+                columns={columnDef}
+                dataSource={mandateTable}
+                pagination={false}
+              />
+            </Col>
+          </Row>
+        </div>
+        <div className={styles.relatedOrder}>
+          <Collapse
+            className={styles.collapseLabel}
+            ghost
+            expandIcon={({ isActive }) => (
+              <UpOutlined
+                style={{ color: '#5697e4' }}
+                rotate={isActive ? 180 : 0}
+              />
+            )}
+            items={handledWorkOrder}
+          />
+        </div>
+      </div>
+    </PageContent>
+  );
+}
+
+export default connect(({ taskUser }) => {
+  return {
+    userList: taskUser.userList,
+  };
+})(TaskDetail);

+ 440 - 0
src/pages/Center/MyTask/Detail/WorkOrderDetail.js

@@ -0,0 +1,440 @@
+import PageContent from '@/components/PageContent';
+import PageTitle from '@/components/PageTitle';
+import SubTitle from '@/pages/TaskManage/components/SubTitle';
+import { OrderStatus, OrderType } from '@/pages/TaskManage/constent';
+import {
+  getCraftRecordList,
+  getMaintainRecordList,
+  getPatrolMandateRecord,
+  getRepairRecordList,
+  getWorkOrderFlow,
+  queryReagentDetail,
+} from '@/services/TaskManage';
+import { useLocation } from '@@/exports';
+import { connect, useRequest } from '@umijs/max';
+import { Col, Row, Steps } from 'antd';
+import dayjs from 'dayjs';
+import { useEffect, useState } from 'react';
+import styles from './workOrderDetail.less';
+// @ts-ignore
+import ReactZmage from 'react-zmage';
+
+const WorkOrderDetail = (props) => {
+  const { userList, dispatch } = props;
+
+  const location = useLocation();
+  const queryParams = new URLSearchParams(location.search);
+  const project_id = Number(queryParams.get('project_id'));
+  const order_id = Number(queryParams.get('order_id'));
+  const order_type = Number(queryParams.get('order_type'));
+  // const mandate_class = Number(queryParams.get('mandate_class'));
+
+  const [orderInfo, setOrderInfo] = useState();
+  const [additionalInfo, setAdditionalInfo] = useState({});
+  const [stepInfo, setStepInfo] = useState();
+
+  // 根据type请求详情
+  const { run: getMaintainDetail } = useRequest(getMaintainRecordList, {
+    manual: true,
+    formatResult: (result) => {
+      const temp = result.data.list[0];
+      const tempDetail = {
+        CreateTime: temp?.CreateTime
+          ? dayjs(temp?.CreateTime).format('YYYY-MM-DD HH:mm')
+          : '-',
+        PlanTime: temp?.PlanTime
+          ? dayjs(temp.PlanTime).format('YYYY-MM-DD HH:mm')
+          : '-',
+        RepairTime: temp?.RepairTime
+          ? dayjs(temp.RepairTime).format('YYYY-MM-DD HH:mm')
+          : '-',
+        Reason: temp.Note,
+        Lubrication: temp.Lubrication,
+        Fasten: temp.Fasten,
+        RustRemoval: temp.RustRemoval,
+        AntiCorrosive: temp.Anticorrosive,
+        Clean: temp.Clean,
+        Check: temp.Check,
+        Repairman:
+          userList.find((item) => item.ID === temp.MaintenancePerson) || '-',
+        DispatchMan:
+          userList.find(
+            (item) => (item.ID = temp.Operators[0]?.Operator?.ID),
+          ) || '-',
+        OrderStatus:
+          OrderStatus.find((item) => item.value === temp.Status) || '-',
+        MandateImages:
+          temp?.mandate_images?.length > 0
+            ? temp.mandate_images.map((item) => {
+                if (item.type === 2) {
+                  return {
+                    src: item.val,
+                  };
+                }
+                return {
+                  src: `data:image/png;base64,${item.val}`,
+                };
+              })
+            : [],
+      };
+      setOrderInfo(tempDetail);
+    },
+  });
+
+  // 根据type请求详情
+  const { run: getRepairDetail } = useRequest(getRepairRecordList, {
+    manual: true,
+    formatResult: (result) => {
+      const temp = result.data.list[0];
+      if (temp === undefined) {
+        return;
+      }
+      const tempDetail = {
+        CreateTime: temp.CreateTime
+          ? dayjs(temp.CreateTime).format('YYYY-MM-DD HH:mm')
+          : '-',
+        PlanTime: temp.PlanTime
+          ? dayjs(temp.PlanTime).format('YYYY-MM-DD HH:mm')
+          : '-',
+        RepairTime: temp.RepairTime
+          ? dayjs(temp.RepairTime).format('YYYY-MM-DD HH:mm')
+          : '-',
+        Reason: temp.Reason,
+        Repairman: userList.find((item) => item.ID === temp.Repairman) || '-',
+        DispatchMan:
+          userList.find((item) => item.ID === temp.operator_id) || '-',
+        OrderStatus:
+          OrderStatus.find((item) => item.value === temp.AcceptanceStatus) ||
+          '-',
+        MandateImages:
+          temp?.mandate_images?.length > 0
+            ? temp.mandate_images.map((item) => {
+                console.log(item);
+                if (item.type === 2) {
+                  return {
+                    src: item.val,
+                  };
+                }
+                return {
+                  src: `data:image/png;base64,${item.val}`,
+                };
+              })
+            : [],
+      };
+      setOrderInfo(tempDetail);
+    },
+  });
+
+  // 根据type请求详情
+  const { run: getCraftDetail } = useRequest(getCraftRecordList, {
+    manual: true,
+    formatResult: (result) => {
+      const temp = result.data.list[0];
+      const tempDetail = {
+        CreateTime: dayjs(temp.start_time).format('YYYY-MM-DD HH:mm'),
+        PlanTime: dayjs(temp.plan_end_time).format('YYYY-MM-DD HH:mm'),
+        RepairTime:
+          (temp.actual_end_time &&
+            dayjs(temp.actual_end_time).format('YYYY-MM-DD HH:mm')) ||
+          '-',
+        Reason: temp.detail,
+        Repairman: userList.find((item) => item.ID === temp.operator_id) || '-',
+        DispatchMan:
+          userList.find((item) => item.ID === temp.checker_id) || '-',
+        OrderStatus:
+          OrderStatus.find((item) => item.value === temp.status) || '-',
+        MandateImages:
+          temp?.mandate_images?.length > 0
+            ? temp.mandate_images.map((item) => {
+                console.log(item);
+                if (item.type === 2) {
+                  return {
+                    src: item.val,
+                  };
+                }
+                return {
+                  src: `data:image/png;base64,${item.val}`,
+                };
+              })
+            : [],
+      };
+
+      setOrderInfo(tempDetail);
+    },
+  });
+
+  const { run: getDosingOrder } = useRequest(queryReagentDetail, {
+    manual: true,
+    formatResult: (result) => {
+      const temp = {
+        ...result,
+        CreateTime: result?.start_time?.Valid
+          ? dayjs(result?.start_time?.Time).format('YYYY-MM-DD HH:mm')
+          : '-',
+        PlanTime: result?.plan_end_time?.Valid
+          ? dayjs(result.plan_end_time.Time).format('YYYY-MM-DD HH:mm')
+          : '-',
+        RepairTime: result?.actual_end_time?.Valid
+          ? dayjs(result.actual_end_time.Time).format('YYYY-MM-DD HH:mm')
+          : '-',
+        Reason: result.note,
+        Repairman:
+          userList.find((user) => user.ID === result.operator_id) || '-',
+        DispatchMan: '-',
+        OrderStatus:
+          OrderStatus.find((status) => status.value === result.status) || '-',
+      };
+      setOrderInfo(temp);
+      setAdditionalInfo(temp);
+    },
+  });
+
+  const { run: getPatrolOrderList } = useRequest(getPatrolMandateRecord, {
+    manual: true,
+    formatResult: (result) => {
+      if (result?.data?.list) {
+        const temp = result.data.list[0];
+        const tempDetail = {
+          ...temp,
+          CreateTime: temp?.CreatedTime
+            ? dayjs(temp.CreatedTime).format('YYYY-MM-DD HH:mm')
+            : '-',
+          PlanTime: temp.plan_end_time
+            ? dayjs(temp.plan_end_time).format('YYYY-MM-DD HH:mm')
+            : '-',
+          RepairTime: temp?.actual_end_time
+            ? dayjs(temp.actual_end_time).format('YYYY-MM-DD HH:mm')
+            : '-',
+          Reason: temp.detail,
+          Repairman:
+            userList.find((user) => user.ID === temp.operator_id) || '-',
+          DispatchMan: '-',
+          OrderStatus:
+            OrderStatus.find((status) => status.value === temp.status) || '-',
+        };
+        setOrderInfo(tempDetail);
+      }
+    },
+  });
+
+  // 获取工单流程信息
+  useRequest(getWorkOrderFlow, {
+    // manual: true,
+    defaultParams: [{ work_type: order_type, work_id: order_id }],
+    formatResult(res) {
+      if (res && res?.length) {
+        setStepInfo(res);
+      }
+    },
+  });
+
+  useEffect(() => {
+    if (userList.length === 0) {
+      dispatch({
+        type: 'taskUser/fetchUserList',
+        payload: { project_id },
+      });
+    }
+    switch (order_type) {
+      // 工艺
+      case 1:
+      case 6:
+      case 7:
+        getCraftDetail({ project_id, work_id: order_id });
+        break;
+      // 维修
+      case 2:
+        getRepairDetail({ project_id, id: order_id });
+        break;
+      // 保养
+      case 3:
+        getMaintainDetail({ project_id, id: order_id });
+        break;
+      // 巡检
+      case 4:
+        getPatrolOrderList({ project_id, id: order_id });
+        break;
+      // 加药
+      case 5:
+        getDosingOrder({ id: order_id });
+        break;
+    }
+  }, []);
+
+  const renderImg = () => {
+    return (
+      <Col span={18}>
+        {orderInfo?.MandateImages?.length > 0 &&
+          orderInfo?.MandateImages?.map((item, index) => {
+            return (
+              <ReactZmage
+                key={index}
+                controller={{
+                  close: true,
+                  flip: true,
+                  zoom: true,
+                  pagination: true,
+                  download: false,
+                }}
+                backdrop="rgba(255,255,255,0.5)"
+                style={{
+                  maxWidth: `${Math.floor(
+                    100 / orderInfo?.MandateImages?.length,
+                  )}%`,
+                }}
+                src={item.src}
+                set={orderInfo?.MandateImages}
+                defaultPage={index}
+              />
+            );
+          })}
+      </Col>
+    );
+  };
+
+  return (
+    <PageContent closeable={false}>
+      <PageTitle returnable>工单详情</PageTitle>
+      <div className={styles.selfCardBox}>
+        <div className={styles.orderInfo}>
+          <SubTitle
+            title="工单信息"
+            // @ts-ignore
+            showStatus={orderInfo?.OrderStatus.value === 2}
+            radius
+          />
+          <div style={{ padding: '15px', letterSpacing: '1.5px' }}>
+            <Row className={styles.rowMargin}>
+              <Col className={styles.fontS28} span={15}>
+                {/* @ts-ignore */}
+                工单类型:
+                {OrderType.find((item) => item.value === order_type)?.label ||
+                  '-'}
+              </Col>
+              <Col className={styles.fontS28} span={9}>
+                {/* @ts-ignore */}
+                工单负责人:{orderInfo?.Repairman?.CName || '-'}
+              </Col>
+            </Row>
+            <Row className={styles.rowMargin}>
+              <Col className={styles.fontS28} span={15}>
+                {/*  @ts-ignore */}
+                工单状态:{orderInfo?.OrderStatus?.label}
+              </Col>
+              <Col className={styles.fontS28} span={9}>
+                {/* @ts-ignore */}
+                派单人员:{orderInfo?.DispatchMan?.CName || '-'}
+              </Col>
+            </Row>
+            <Row className={styles.rowMargin}>
+              <Col className={styles.fontS28}>
+                派单时间:{orderInfo?.CreateTime || '-'}
+              </Col>
+            </Row>
+            <Row className={styles.rowMargin}>
+              <Col className={styles.fontS28}>
+                计划完成时间:{orderInfo?.PlanTime || '-'}
+              </Col>
+            </Row>
+            <Row className={styles.rowMargin}>
+              <Col className={styles.fontS28}>
+                实际完成时间:{orderInfo?.RepairTime || '-'}
+              </Col>
+            </Row>
+            <Row>
+              <Col className={styles.fontS28}>工单详情:</Col>
+              <Col className={styles.fontS28} span={18}>
+                {orderInfo?.Reason}
+              </Col>
+            </Row>
+
+            {/* @ts-ignore */}
+            {orderInfo?.MandateImages?.length > 0 && (
+              <Row className={styles.rowMarginTop}>
+                <Col className={styles.fontS28}>任务图片:</Col>
+                {renderImg()}
+              </Row>
+            )}
+          </div>
+        </div>
+        {order_type === 3 && (
+          <div>
+            <SubTitle title="维修内容" />
+            <div style={{ padding: '15px' }}>
+              <Row className={styles.rowMargin} justify={'space-around'}>
+                <Col className={styles.fontS28} span={8}>
+                  是否润滑/加油:{orderInfo?.Lubrication === 1 ? '是' : '否'}
+                </Col>
+                <Col className={styles.fontS28} span={8}>
+                  是否拆检:{orderInfo?.Check === 1 ? '是' : '否'}
+                </Col>
+                <Col className={styles.fontS28} span={8}>
+                  是否清洁:{orderInfo?.Clean === 1 ? '是' : '否'}
+                </Col>
+              </Row>
+              <Row justify={'space-around'}>
+                <Col className={styles.fontS28} span={8}>
+                  是否紧固:{orderInfo?.Fasten === 1 ? '是' : '否'}
+                </Col>
+                <Col className={styles.fontS28} span={8}>
+                  是否除锈:{orderInfo?.AntiCorrosive === 1 ? '是' : '否'}
+                </Col>
+                <Col className={styles.fontS28} span={8}>
+                  是否防腐:{orderInfo?.RustRemoval === 1 ? '是' : '否'}
+                </Col>
+              </Row>
+            </div>
+          </div>
+        )}
+
+        {order_type === 5 && (
+          <div>
+            <SubTitle title="加药详情" />
+            <div style={{ padding: '15px' }}>
+              <Row>
+                <Col className={styles.fontS28} span={8}>
+                  药剂名称:{additionalInfo?.name || '-'}
+                </Col>
+                <Col className={styles.fontS28} span={8}>
+                  加药量:{additionalInfo?.dosage || '-'}升
+                </Col>
+                <Col className={styles.fontS28} span={8}>
+                  浓度:{additionalInfo?.concentration || '-'}
+                </Col>
+              </Row>
+            </div>
+          </div>
+        )}
+        {/* @ts-ignore */}
+        {stepInfo?.length > 0 && (
+          <div>
+            <SubTitle title="工单流程" />
+            <div style={{ padding: '15px 20px' }}>
+              <Steps
+                direction="vertical"
+                current={stepInfo?.length ? stepInfo.length - 1 : 0}
+                progressDot
+                items={stepInfo?.map((item) => {
+                  return {
+                    title: (
+                      <span className={styles.fontS28}>{item.content}</span>
+                    ),
+                    description: (
+                      <span className={styles.fontS24}>{item.time}</span>
+                    ),
+                  };
+                })}
+              />
+            </div>
+          </div>
+        )}
+      </div>
+    </PageContent>
+  );
+};
+
+export default connect(({ taskUser }) => {
+  return {
+    userList: taskUser.userList,
+  };
+})(WorkOrderDetail);

+ 125 - 0
src/pages/Center/MyTask/Detail/taskDetail.less

@@ -0,0 +1,125 @@
+.cardContainer {
+  margin-top: 15px;
+  padding: 20px 20px;
+  background-color: white;
+
+  .normalInfo {
+    padding: 25px 15px;
+    background-color: #fdf2df;
+  }
+
+  .infoRow {
+    margin-bottom: 25px;
+  }
+
+  .detailInfo {
+    padding: 25px 15px;
+    border-bottom: 1px solid rgba(0, 0, 0, 10%);
+  }
+
+  .relatedOrder {
+    padding: 0 15px;
+    margin-top: 15px;
+
+    .collapseLabel {
+      width: 100%;
+
+      .ant-collapse-arrow {
+        font-size: 24px;
+      }
+
+      :global {
+        .ant-collapse-item {
+          margin-bottom: 0;
+        }
+
+        .ant-collapse-header {
+          justify-content: center;
+          flex-direction: row-reverse;
+          align-items: center;
+          margin-top: 10px;
+
+          .ant-collapse-header-text {
+            font-size: 24px;
+            flex: unset;
+            margin-inline-end: unset;
+          }
+
+          .ant-collapse-arrow {
+            font-size: 24px;
+          }
+        }
+      }
+
+      .workOrderCard {
+        margin-bottom: 20px;
+        padding: 20px 10px;
+        border-radius: 8px;
+        background-color: #e5effa;
+        display: flex;
+        align-items: center;
+
+        .leftInfo {
+          width: 80%;
+        }
+
+        .rightButton {
+          flex: auto;
+          color: #5697e4;
+          font-size: 24px;
+          text-align: center;
+          display: flex;
+          justify-content: center;
+          align-items: center;
+        }
+      }
+    }
+  }
+
+  .workOrderCard {
+    margin-bottom: 25px;
+    padding: 20px 10px;
+    border-radius: 8px;
+    background-color: #e5effa;
+    display: flex;
+    align-items: center;
+
+    .leftInfo {
+      width: 80%;
+    }
+
+    .rightButton {
+      flex: auto;
+      color: #5697e4;
+      font-size: 24px;
+      text-align: center;
+      display: flex;
+      justify-content: center;
+      align-items: center;
+    }
+  }
+}
+
+.fontS28 {
+  font-size: 28px;
+}
+
+.fontS24 {
+  font-size: 24px;
+}
+
+.fontS22 {
+  font-size: 22px;
+}
+
+.fontS20 {
+  font-size: 20px;
+}
+
+.fontS18 {
+  font-size: 18px;
+}
+
+.fontS16 {
+  font-size: 16px;
+}

+ 47 - 0
src/pages/Center/MyTask/Detail/workOrderDetail.less

@@ -0,0 +1,47 @@
+.selfCardBox {
+  margin-top: 15px;
+  border-radius: 8px;
+  box-shadow: 2px 0 8px 0 rgba(0, 0, 0, 30%);
+  background-color: white;
+}
+
+.orderInfo {
+  border: 0;
+  margin: 0;
+}
+
+.rowMargin {
+  margin-bottom: 30px;
+}
+
+.rowMarginTop {
+  margin-top: 30px;
+}
+
+.fontS28 {
+  font-size: 28px;
+}
+
+.fontS26 {
+  font-size: 26px;
+}
+
+.fontS24 {
+  font-size: 24px;
+}
+
+.fontS22 {
+  font-size: 22px;
+}
+
+.fontS20 {
+  font-size: 20px;
+}
+
+.fontS18 {
+  font-size: 18px;
+}
+
+.fontS16 {
+  font-size: 16px;
+}

+ 308 - 0
src/pages/Center/MyTask/List/TaskList.js

@@ -0,0 +1,308 @@
+import PageContent from '@/components/PageContent';
+import PageTitle from '@/components/PageTitle';
+import ScrollLoading from '@/components/ScrollLoading';
+import TopFilter from '@/pages/TaskManage/components/TopFilter';
+import {
+  MandateClass,
+  MandateStatus,
+  MandateType,
+  OrderStatus,
+  OrderType,
+} from '@/pages/TaskManage/constent';
+import { getMandateList } from '@/services/TaskManage';
+import { DownOutlined } from '@ant-design/icons';
+import { connect, useLocation, useNavigate, useRequest } from '@umijs/max';
+import { Col, Collapse, Divider, List, Row } from 'antd';
+import dayjs from 'dayjs';
+import { useEffect, useState } from 'react';
+import styles from './taskList.less';
+
+const MyTaskList = (props) => {
+  const { userList, loading, dispatch } = props;
+
+  const location = useLocation();
+
+  const queryParams = new URLSearchParams(location.search);
+  const project_id = Number(queryParams.get('project_id'));
+  const mandateType = Number(queryParams.get('mandateType'));
+  const userID = queryParams.get('user_id');
+
+  const navigate = useNavigate();
+
+  const [currentParams, setCurrentParams] = useState({
+    project_id,
+    mandate_type: mandateType,
+    pageSize: 20,
+    currentPage: 1,
+    responsible_people: userID !== null ? Number(userID) : '',
+  });
+  const [pagination, setPagination] = useState({
+    current: 1,
+    total: 0,
+    pageSize: 20,
+  });
+  const [topFiltersConfig, setTopFiltersConfig] = useState([]);
+  const [mandateList, setMandateList] = useState([]);
+
+  // 获取用户
+  useEffect(() => {
+    if (userList.length === 0) {
+      dispatch({
+        type: 'taskUser/fetchUserList',
+        payload: { project_id },
+      });
+    }
+  }, []);
+
+  // 配置顶部下拉过滤器
+  useEffect(() => {
+    const filters = [];
+    filters.push({
+      key: 'mandate_class',
+      placeholder: '任务类别',
+      // @ts-ignore
+      options: MandateClass.map((item) => {
+        if (item.MandateType === mandateType) {
+          return {
+            value: item.value,
+            label: item.label,
+            key: item.value + '任务类别',
+          };
+        }
+        return undefined;
+      }).filter((item) => item),
+    });
+
+    filters.push({
+      key: 'status',
+      placeholder: '任务状态',
+      options: MandateStatus.map((item) => {
+        return {
+          ...item,
+        };
+      }),
+    });
+
+    setTopFiltersConfig(filters);
+  }, [mandateType]);
+
+  const { run: getList, loading: loadData } = useRequest(getMandateList, {
+    defaultParams: [currentParams],
+    formatResult: (result) => {
+      const pageInfo = result.data.pagination;
+      if (result.data.pagination.current === 1) {
+        setMandateList(result.data.list);
+      } else {
+        if (mandateList.length < pageInfo.total) {
+          setMandateList([...mandateList, ...result.data.list]);
+        }
+      }
+      setPagination(pageInfo);
+    },
+  });
+
+  const onTopFilterChange = (value) => {
+    if (topFiltersConfig.length === 0) {
+      return;
+    }
+    const params = {
+      project_id,
+      mandate_type: mandateType,
+      pageSize: 20,
+      currentPage: 1,
+    };
+
+    if (userID !== null) {
+      params.responsible_people = Number(userID);
+    }
+
+    for (let i = 0; i < value.length; i++) {
+      if (value[i] !== null && topFiltersConfig[i] !== undefined) {
+        params[topFiltersConfig[i].key] = value[i];
+      }
+    }
+    setCurrentParams(params);
+    getList(params);
+  };
+
+  const goMyTaskDetail = (mandate) => {
+    navigate(
+      `/center/my-task/task-detail?project_id=${project_id}&mandate_id=${mandate.Id}`,
+    );
+  };
+
+  const goMyWorkOrder = (orderID, orderType, mandateClass) => {
+    if (orderType === undefined) {
+      return;
+    }
+    navigate(
+      `/center/my-task/work-order-detail?project_id=${project_id}&order_id=${orderID}&order_type=${orderType}&mandate_class=${mandateClass}`,
+    );
+  };
+
+  const buildTaskList = (item) => {
+    const formatItem = {
+      ...item,
+      Status: MandateStatus.find((status) => status.value === item.Status),
+      MandateType: MandateType.find((type) => type.value === item.MandateType),
+      MandateClass: MandateClass.find(
+        (itemClass) => itemClass.value === item.MandateClass,
+      ),
+      ResponsiblePeople: userList.find(
+        (user) => user.ID === item.ResponsiblePeople,
+      ),
+      CreateTime: dayjs(item.CreateTime).format('YYYY-MM-DD HH:mm'),
+    };
+
+    const workOrder = item.Records.map((record) => {
+      return {
+        ...record,
+        key: record.Id,
+        Status: OrderStatus.find((status) => status.value === record.Status),
+        RecordType: OrderType.find((type) => type.value === record.RecordType),
+        Responsible: userList.find((user) => user.ID === record.Responsible),
+        CreateTime: dayjs(record.CreateTime).format('YYYY-MM-DD HH:mm'),
+      };
+    });
+
+    const collapseData = [
+      {
+        key: '1',
+        label: (
+          <span style={{ color: '#5697e4' }}>关联工单({workOrder.length})</span>
+        ),
+        children: workOrder.map((order) => {
+          return (
+            <div key={order.Id} className={styles.workOrderCard}>
+              <div className={styles.leftInfo}>
+                <Row style={{ marginBottom: '15px' }}>
+                  <Col className={styles.fontS24} span={12}>
+                    工单类型:{order.RecordType?.label || '-'}
+                  </Col>
+                  <Col className={styles.fontS24} span={12}>
+                    时间:{order.CreateTime}
+                  </Col>
+                </Row>
+                <Row>
+                  <Col className={styles.fontS24} span={12}>
+                    工单状态:
+                    <span style={{ color: '#5697e4' }}>
+                      {order.Status?.label}
+                    </span>
+                  </Col>
+                  <Col className={styles.fontS24} span={12}>
+                    工单负责人:{order.Responsible?.CName}
+                  </Col>
+                </Row>
+              </div>
+              <Divider type="vertical" style={{ height: '40px' }} />
+              <div
+                className={styles.rightButton}
+                style={{ color: '#5697e4' }}
+                onClick={() => {
+                  goMyWorkOrder(
+                    order.Id,
+                    order.RecordType?.value,
+                    item.MandateClass,
+                  );
+                }}
+              >
+                查看工单
+              </div>
+            </div>
+          );
+        }),
+      },
+    ];
+
+    return (
+      <List.Item style={{ borderBottom: '0' }}>
+        <div className={`${styles.cardContainer} card-box`}>
+          <Row justify="space-between" style={{ marginBottom: '20px' }}>
+            <Col className={styles.fontS24}>时间:{formatItem.CreateTime}</Col>
+            <Col className={styles.fontS24}>
+              任务类别:{formatItem.MandateClass?.label}
+            </Col>
+            <Col className={styles.fontS24}>
+              任务负责人:{formatItem.ResponsiblePeople?.CName || '-'}
+            </Col>
+          </Row>
+          <Row
+            justify="space-between"
+            style={{
+              paddingBottom: '10px',
+              borderBottom: '1px solid #D5D5D5',
+            }}
+          >
+            <Col className={styles.fontS24}>
+              任务状态:{formatItem.Status?.label || '-'}
+            </Col>
+            <Col>
+              <div
+                className={styles.fontS24}
+                style={{
+                  backgroundColor: '#f5a623',
+                  color: 'white',
+                  width: '150px',
+                  height: '50px',
+                  display: 'flex',
+                  justifyContent: 'center',
+                  alignItems: 'center',
+                }}
+                onClick={() => {
+                  goMyTaskDetail(item);
+                }}
+              >
+                任务详情
+              </div>
+            </Col>
+          </Row>
+          <Row>
+            <Collapse
+              className={styles.collapseLabel}
+              ghost
+              expandIcon={({ isActive }) => (
+                <DownOutlined
+                  style={{ color: '#5697e4' }}
+                  rotate={isActive ? 180 : 0}
+                />
+              )}
+              items={collapseData}
+            />
+          </Row>
+        </div>
+      </List.Item>
+    );
+  };
+
+  return (
+    <PageContent closeable={false}>
+      <PageTitle returnable>
+        {MandateType.find((item) => item.value === mandateType)?.label}
+      </PageTitle>
+      <TopFilter filters={topFiltersConfig} onChange={onTopFilterChange} />
+
+      <ScrollLoading
+        height={180}
+        loading={loading || loadData}
+        pagination={pagination}
+        handleLoadData={(current) => {
+          getList({ ...currentParams, currentPage: current });
+        }}
+      >
+        <List
+          itemLayout="horizontal"
+          dataSource={mandateList}
+          renderItem={buildTaskList}
+        />
+      </ScrollLoading>
+    </PageContent>
+  );
+};
+
+export default connect(({ taskUser, loading }) => {
+  return {
+    userList: taskUser.userList,
+    loading: loading.models['taskUser'],
+  };
+})(MyTaskList);

+ 168 - 0
src/pages/Center/MyTask/List/WorkOrderList.js

@@ -0,0 +1,168 @@
+import PageContent from '@/components/PageContent';
+import PageTitle from '@/components/PageTitle';
+import { OrderStatus, OrderType } from '@/pages/TaskManage/constent';
+import { getUserWorkOrderList } from '@/services/TaskManage';
+import { connect, useLocation, useNavigate, useRequest } from '@umijs/max';
+import { Button, Col, Empty, Row, Spin, Tabs } from 'antd';
+import dayjs from 'dayjs';
+import { useEffect, useState } from 'react';
+import styles from './WorkOrderList.less';
+
+const { TabPane } = Tabs;
+
+const WorkOrderList = (props) => {
+  const { userList, dispatch } = props;
+  const location = useLocation();
+  const queryParams = new URLSearchParams(location.search);
+  const project_id = Number(queryParams.get('project_id'));
+  const order_type = Number(queryParams.get('order_type'));
+  const userID = queryParams.get('user_id');
+
+  const navigate = useNavigate();
+
+  // 请求用户列表
+  useEffect(() => {
+    if (userList.length === 0) {
+      dispatch({
+        type: 'taskUser/fetchUserList',
+        payload: { project_id },
+      });
+    }
+  }, []);
+
+  const [activedStatus, setActivedStatus] = useState();
+
+  const [workOrderListDOM, setWorkOrderList] = useState(null);
+
+  const goWorkOrderDetail = (order) => {
+    const orderID = order.id || order.Id;
+    navigate(
+      `/center/my-task/work-order-detail?project_id=${project_id}&order_id=${orderID}&order_type=${order_type}`,
+    );
+  };
+
+  const renderTabPannal = (list) => {
+    if (list?.length === 0) {
+      setWorkOrderList(<Empty />);
+      return;
+    }
+    const tempNode = list.map((workOrder) => {
+      return (
+        <div
+          key={workOrder.id || workOrder.Id}
+          className={styles.workOrderCard}
+        >
+          <Row justify="space-between" style={{ marginBottom: '20px' }}>
+            <Col className={styles.fontS24}>{workOrder?.CreateTime}</Col>
+            <Col className={styles.fontS24}>
+              工单负责人:{workOrder?.Repairman?.CName || '-'}
+            </Col>
+          </Row>
+          <Row justify="space-between">
+            <Col className={styles.fontS24}>
+              工单状态:{workOrder?.OrderStatus?.label}
+            </Col>
+            <Col className={styles.detailBtnContainer}>
+              <Button
+                type="primary"
+                style={{ height: '40px', fontSize: '24px' }}
+                onClick={() => goWorkOrderDetail(workOrder)}
+              >
+                工单详情
+              </Button>
+            </Col>
+          </Row>
+        </div>
+      );
+    });
+    setWorkOrderList(tempNode);
+  };
+
+  const formatCreateTime = (order) => {
+    if (order_type === 1 || order_type === 6 || order_type === 7) {
+      if (order?.start_time) {
+        return dayjs(order.start_time).format('YYYY-MM-DD HH:mm');
+      }
+    } else if (order_type === 2 || order_type === 3) {
+      if (order?.CreateTime) {
+        return dayjs(order.CreateTime).format('YYYY-MM-DD HH:mm');
+      }
+    } else if (order_type === 5) {
+      if (order?.start_time?.Valid) {
+        return dayjs(order.start_time.Time).format('YYYY-MM-DD HH:mm');
+      }
+    }
+    return '-';
+  };
+
+  const { run: getWorkOrderList, loading } = useRequest(
+    (status) => {
+      return getUserWorkOrderList({
+        project_id,
+        work_type: order_type,
+        status: status || 0,
+        user_id: Number(userID),
+      });
+    },
+    {
+      throwOnError: true,
+      formatResult: (result) => {
+        if (result) {
+          const temp = result.map((item) => {
+            return {
+              ...item,
+              CreateTime: item.CreateTime
+                ? dayjs(item.CreateTime).format('YYYY-MM-DD HH:mm')
+                : '-',
+              Repairman: userList.find((user) => item.Responsible === user.ID),
+              OrderStatus: OrderStatus.find(
+                (status) => status.value === item.Status,
+              ),
+            };
+          });
+          renderTabPannal(temp);
+        }
+      },
+    },
+  );
+
+  const onTabChange = (key) => {
+    setActivedStatus(key);
+    getWorkOrderList(Number(key));
+  };
+
+  return (
+    <PageContent closeable={false}>
+      <PageTitle returnable>
+        {OrderType.find((item) => item.value === order_type)?.label || ''}
+      </PageTitle>
+      <Tabs
+        size="large"
+        activeKey={activedStatus}
+        defaultActiveKey={String(OrderStatus[0].value)}
+        className={styles.identifyTab}
+        onChange={onTabChange}
+      >
+        {OrderStatus.map((item) => {
+          return (
+            <TabPane tab={item.label} key={String(item.value)}>
+              <Spin spinning={loading}>
+                <div
+                  style={{ height: 'calc(100vh - 180px)', overflowY: 'auto' }}
+                >
+                  {workOrderListDOM}
+                </div>
+              </Spin>
+            </TabPane>
+          );
+        })}
+      </Tabs>
+    </PageContent>
+  );
+};
+
+export default connect(({ taskUser }) => {
+  return {
+    userList: taskUser.userList,
+  };
+})(WorkOrderList);

+ 29 - 0
src/pages/Center/MyTask/List/WorkOrderList.less

@@ -0,0 +1,29 @@
+.identifyTab :global {
+  .ant-tabs-nav-list {
+    justify-content: space-around;
+    width: 100%;
+    .ant-tabs-tab-btn {
+      font-size: 24px;
+    }
+  }
+}
+
+.workOrderCard {
+  background-color: white;
+  padding: 20px;
+  margin-top: 20px;
+  margin-bottom: 10px;
+  margin-left: 5px;
+  border-radius: 10px;
+  box-shadow: 0px 0px 8px 2px rgba(191, 191, 191, 0.2);
+}
+
+.detailBtnContainer {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+}
+
+.fontS24 {
+  font-size: 24px;
+}

+ 91 - 0
src/pages/Center/MyTask/List/taskList.less

@@ -0,0 +1,91 @@
+.cardContainer {
+  width: 100%;
+  padding: 20px 20px 10px 20px;
+  margin-left: 5px;
+  background-color: white;
+
+  .collapseLabel {
+    width: 100%;
+    .ant-collapse-arrow {
+      font-size: 24px;
+    }
+    :global {
+      .ant-collapse-item {
+        margin-bottom: 0;
+      }
+      .ant-collapse-header {
+        justify-content: center;
+        flex-direction: row-reverse;
+        align-items: center;
+        margin-top: 10px;
+
+        .ant-collapse-header-text {
+          font-size: 24px;
+          flex: unset;
+          margin-inline-end: unset;
+        }
+        .ant-collapse-arrow {
+          font-size: 24px;
+        }
+      }
+    }
+
+    .workOrderCard {
+      margin-bottom: 20px;
+      padding: 20px 10px;
+      border-radius: 8px;
+      background-color: #e5effa;
+      display: flex;
+      align-items: center;
+
+      .leftInfo {
+        width: 80%;
+      }
+
+      .rightButton {
+        flex: auto;
+        color: #5697e4;
+        font-size: 24px;
+        text-align: center;
+        display: flex;
+        justify-content: center;
+        align-items: center;
+      }
+    }
+  }
+}
+
+.topContainer {
+  // height: 120px;
+  .fixedTop {
+    position: fixed;
+    top: 0;
+    width: 100%;
+    z-index: 10;
+    background-color: #ffffff;
+  }
+}
+
+.fontS28 {
+  font-size: 28px;
+}
+
+.fontS26 {
+  font-size: 26px;
+}
+
+.fontS24 {
+  font-size: 24px;
+}
+
+.fontS20 {
+  font-size: 20px;
+}
+
+.fontS18 {
+  font-size: 18px;
+}
+
+.fontS16 {
+  font-size: 16px;
+}

+ 178 - 0
src/pages/Center/MyTask/index.js

@@ -0,0 +1,178 @@
+import PageContent from '@/components/PageContent';
+import TabsContent from '@/components/TabsContent';
+import { MandateType, OrderType } from '@/pages/TaskManage/constent';
+import { getMandateList } from '@/services/TaskManage';
+import { RightOutlined } from '@ant-design/icons';
+import { connect, useLocation, useNavigate, useParams } from '@umijs/max';
+import { List, Spin } from 'antd';
+import { useEffect, useState } from 'react';
+import styles from './index.less';
+
+const MyTask = (props) => {
+  const { userList, dispatch } = props;
+  const { projectId } = useParams();
+  const project_id = Number(projectId === '' ? '0' : projectId);
+
+  const location = useLocation();
+  const queryParams = new URLSearchParams(location.search);
+  const userID = queryParams.get('user_id');
+
+  const navigate = useNavigate();
+
+  const [mandateCount, setMandateCount] = useState([0, 0, 0]);
+  const [loading, setLoading] = useState(false);
+  const [tab, setTab] = useState(localStorage.taskTab || '1');
+
+  useEffect(() => {
+    const requests = [];
+    for (let i = 0; i < 4; i++) {
+      requests.push(
+        getMandateList({
+          project_id,
+          pageSize: 1,
+          currentPage: 1,
+          mandate_type: i + 1,
+          responsible_people: userID !== null ? Number(userID) : '',
+        }),
+      );
+    }
+    setLoading(true);
+    Promise.all(requests)
+      .then((resList) => {
+        if (resList.filter((item) => item.code !== 200).length) {
+          throw new Error('请求错误');
+        }
+        const typeCount = [0, 0, 0, 0];
+        resList.forEach((item, index) => {
+          typeCount[index] = item.data.pagination?.total;
+        });
+        setMandateCount(typeCount);
+      })
+      .catch((err) => {
+        console.log(err);
+      })
+      .finally(() => {
+        setLoading(false);
+      });
+  }, []);
+
+  useEffect(() => {
+    if (userList.length === 0) {
+      dispatch({
+        type: 'taskUser/fetchUserList',
+        payload: { project_id },
+      });
+    }
+  }, []);
+
+  const onTabChange = (key) => {
+    setTab(key);
+    localStorage.setItem('taskTab', key);
+  };
+
+  const goTaskList = (item) => {
+    navigate(
+      `/center/my-task/task-list?project_id=${project_id}&mandateType=${item}${
+        userID !== null ? '&user_id=' + userID : ''
+      }`,
+    );
+  };
+  const goWorkOrderList = (item) => {
+    navigate(
+      `/center/my-task/work-order-list?project_id=${project_id}&order_type=${item}${
+        userID !== null ? '&user_id=' + userID : ''
+      }`,
+    );
+  };
+
+  const makeTaskList = (item, index) => {
+    return (
+      <List.Item
+        className={styles.listItem}
+        onClick={() => {
+          goTaskList(item.value);
+        }}
+      >
+        <List.Item.Meta
+          title={<span className={styles.fontS28}>{item.label}</span>}
+        />
+
+        <div className={styles.itemCount}>
+          <div className={styles.countNumber}>{mandateCount[index]}</div>
+          <div className={styles.fontS22}>任务数量</div>
+        </div>
+
+        <RightOutlined />
+      </List.Item>
+    );
+  };
+
+  const makeWorkOrderList = (item) => {
+    return (
+      <List.Item
+        className={styles.listItem}
+        onClick={() => {
+          goWorkOrderList(item.value);
+        }}
+      >
+        <List.Item.Meta
+          title={<span className={styles.fontS28}>{item.label}</span>}
+        />
+        {/* <div className={styles.itemCount}>
+          <div className={styles.countNumber}>{mandateCount[index]}</div>
+          <div className={styles.fontS22}>任务数量</div>
+        </div> */}
+        <RightOutlined />
+      </List.Item>
+    );
+  };
+
+  return (
+    <PageContent tabs closeable={false} returnable>
+      <TabsContent
+        defaultActiveKey={tab}
+        onChange={onTabChange}
+        items={[
+          {
+            label: `我的任务`,
+            key: '1',
+            children: (
+              <Spin spinning={loading}>
+                <List
+                  className={styles.taskList}
+                  bordered
+                  itemLayout="horizontal"
+                  dataSource={MandateType}
+                  renderItem={makeTaskList}
+                  pagination={false}
+                />
+              </Spin>
+            ),
+          },
+          {
+            label: `我的工单`,
+            key: '2',
+            children: (
+              <Spin spinning={false}>
+                <List
+                  className={styles.taskList}
+                  bordered
+                  itemLayout="horizontal"
+                  dataSource={OrderType}
+                  renderItem={makeWorkOrderList}
+                  pagination={false}
+                />
+              </Spin>
+            ),
+          },
+        ]}
+      />
+    </PageContent>
+  );
+};
+
+export default connect(({ taskUser }) => {
+  return {
+    userList: taskUser.userList,
+  };
+})(MyTask);

+ 79 - 0
src/pages/Center/MyTask/index.less

@@ -0,0 +1,79 @@
+.taskList {
+  border: none;
+  max-height: calc(100vh - 140px);
+  overflow-y: scroll;
+
+  .listItem {
+    margin: 20px 20px 20px 5px;
+    height: 120px;
+    box-shadow: 0 0 6px 3px rgba(0, 150, 255, 10%);
+    border-radius: 10px;
+    background-color: #ffffff;
+
+    .itemCount {
+      display: flex;
+      flex-direction: column;
+      align-items: center;
+      margin-right: 30px;
+
+      .countNumber {
+        color: #f5a623;
+        font-size: 32px;
+        font-weight: 500;
+        margin-bottom: 6px;
+      }
+    }
+  }
+}
+
+.ant-select-clear {
+  opacity: 1 !important;
+  margin-top: -12px !important;
+  width: 24px !important;
+  height: 24px !important;
+  font-size: 24px !important;
+  color: black !important;
+}
+
+.antdSelect .ant-select-selection-item,
+.antdSelect .ant-select-selection-placeholder,
+.ant-select-item-option-content {
+  font-size: 28px;
+  color: black;
+  margin-right: 10px;
+}
+.ant-select-item-option {
+  padding: 8px !important;
+}
+.ant-select-arrow {
+  font-size: 24px !important;
+  color: black !important;
+}
+
+.fontS28 {
+  font-size: 28px;
+}
+
+.fontS26 {
+  font-size: 26px;
+}
+
+.fontS24 {
+  font-size: 24px;
+}
+
+.fontS22 {
+  font-size: 22px;
+}
+
+.fontS20 {
+  font-size: 20px;
+}
+
+.fontS18 {
+  font-size: 18px;
+}
+
+.fontS16 {
+  font-size: 16px;
+}

+ 85 - 0
src/pages/Center/index.js

@@ -0,0 +1,85 @@
+const taskIcon = require('@/assets/center/my-task.png');
+const orderIcon = require('@/assets/center/order.png');
+import PageContent from '@/components/PageContent';
+import PageTitle from '@/components/PageTitle';
+import { version } from '@/constants';
+import { UnityAction } from '@/utils/utils';
+import { useModel, useNavigate, useParams } from '@umijs/max';
+import styles from './index.less';
+
+const Center = () => {
+  const { initialState } = useModel('@@initialState');
+  const user = initialState?.user || {};
+  const navigate = useNavigate();
+
+  const { projectId } = useParams();
+
+  const handleGoSystem = () => {
+    navigate(`/system-daily/${projectId}`);
+  };
+  const handleSmartReport = () => {
+    navigate(`/smart-report/${projectId}`);
+  };
+
+  const handleLogOutClick = () => {
+    UnityAction.sendMsg('Logout');
+  };
+  const toMyTask = (type) => {
+    localStorage.taskTab = type;
+    navigate(`/center/my-task/${projectId}?user_id=${user.ID}`);
+  };
+
+  return (
+    <PageContent>
+      <PageTitle>个人中心</PageTitle>
+
+      <div className={styles.head}>
+        <div className={styles.profile} />
+        <div className={styles.textContent}>
+          <div className={styles.name}>{user?.CName}</div>
+          <div className={styles.photo}>
+            手机号:{user?.Mobile}
+            <br />
+            系统版本:
+            {version}
+          </div>
+        </div>
+      </div>
+      <div className={styles.center}>
+        <div
+          className={styles.item}
+          onClick={() => {
+            toMyTask(1);
+          }}
+        >
+          <img src={taskIcon} style={{ marginRight: '20px' }} />
+          我的任务
+        </div>
+        <div
+          className={styles.item}
+          onClick={() => {
+            toMyTask(2);
+          }}
+        >
+          <img src={orderIcon} style={{ marginRight: '20px' }} />
+          我的工单
+        </div>
+      </div>
+      <div className={styles.bottomContent}>
+        <div className={styles.lineItem} onClick={handleGoSystem}>
+          系统报告 <div className={styles.arr}></div>
+        </div>
+        <div className={styles.lineItem} onClick={handleSmartReport}>
+          智慧运营报告
+          <div className={styles.arr}></div>
+        </div>
+        <div className={styles.lineItem}>问题反馈</div>
+        <div className={styles.lineItem}>个人设置</div>
+      </div>
+      <div className={styles.loginOut} onClick={handleLogOutClick}>
+        退出登录
+      </div>
+    </PageContent>
+  );
+};
+export default Center;

+ 104 - 0
src/pages/Center/index.less

@@ -0,0 +1,104 @@
+.page {
+  // margin: auto;
+  width: 100%;
+  padding: 0 200px;
+  background-color: #ffffff;
+}
+.box {
+  background: rgba(255, 255, 255);
+  box-shadow: 0px 0px 8px 4px rgba(212, 212, 212, 0.5);
+  border-radius: 20px;
+}
+.head {
+  .box;
+  position: relative;
+  margin: 30px 0;
+  padding: 64px 76px;
+  display: flex;
+  .profile {
+    width: 150px;
+    height: 150px;
+    background-image: url('@/assets/center/profile.png');
+    background-size: 100%, 100%;
+    background-repeat: no-repeat;
+  }
+  .textContent {
+    margin-left: 60px;
+    display: flex;
+    flex-direction: column;
+    justify-content: space-around;
+    .name {
+      font-size: 34px;
+      font-family: PingFangSC, PingFang SC;
+      font-weight: 400;
+      color: #4a4a4a;
+      line-height: 48px;
+    }
+    .photo {
+      font-size: 28px;
+      font-family: PingFangSC, PingFang SC;
+      font-weight: 400;
+      color: #4a4a4a;
+      line-height: 40px;
+    }
+  }
+}
+.center {
+  margin-bottom: 30px;
+  height: 150px;
+  display: flex;
+  justify-content: space-between;
+  .item {
+    .box;
+    width: 49%;
+    text-align: center;
+    font-size: 32px;
+    line-height: 150px;
+    font-family: PingFangSC, PingFang SC;
+    font-weight: 400;
+    color: #4a4a4a;
+  }
+}
+.bottomContent {
+  .box;
+  padding: 10px 30px;
+  font-size: 32px;
+  font-family: PingFangSC, PingFang SC;
+  font-weight: 400;
+  color: #4a4a4a;
+  .lineItem {
+    height: 100px;
+    line-height: 100px;
+    border-bottom: 1px solid #d4d4d4;
+    .arr {
+      width: 28px;
+      height: 40px;
+      background: url('@/assets/center/icon-arr.png') no-repeat center;
+      background-size: 100% 100%;
+      float: right;
+      margin-top: 30px;
+    }
+  }
+  .lineItem:last-child {
+    border-bottom: none;
+  }
+}
+.loginOut {
+  margin: 50px auto 0;
+  width: 329px;
+  height: 97px;
+  text-align: center;
+  background: rgba(74, 144, 226);
+  border-radius: 49px;
+  font-size: 32px;
+  font-family: PingFangSC, PingFang SC;
+  font-weight: 400;
+  color: #ffffff;
+  line-height: 97px;
+}
+.close {
+  position: absolute;
+  top: 20px;
+  right: 20px;
+  font-size: 30px;
+}

+ 6 - 0
src/pages/Controller/Device.js

@@ -46,6 +46,10 @@ function Hardware() {
     return () => clearInterval(timer);
   }, [draw]);
 
+  const onHandleClick = (item) => {
+    UnityAction.sendMsg('SingleDevLocate', JSON.stringify([item]));
+  };
+
   return (
     <PageContent closeable={false}>
       <PageTitle onReturn={() => UnityAction.sendMsg('menuItem', '智能管控')}>
@@ -66,6 +70,8 @@ function Hardware() {
             ></i>
             {item.mobile_device_name}
           </div>
+
+          <div onClick={() => onHandleClick(item)}>定位</div>
         </div>
       ))}
       {/* </Spin> */}

+ 1 - 1
src/pages/Controller/index.js

@@ -1,7 +1,7 @@
 import PageContent from '@/components/PageContent';
 import PageTitle from '@/components/PageTitle';
 import { UnityAction } from '@/utils/utils';
-import { history, useParams } from '@umijs/max';
+import { useParams } from '@umijs/max';
 import styles from './index.less';
 
 const HardwareController = (props) => {

+ 174 - 43
src/pages/DeviceManager/detail.js

@@ -7,13 +7,13 @@ import {
 import { useParams, useRequest } from '@umijs/max';
 import { Table } from 'antd';
 import dayjs from 'dayjs';
-import { useEffect } from 'react';
+import { useEffect, useRef, useState } from 'react';
 import { PageType } from '.';
 const SparePartDetail = () => {
   const { projectId, type } = useParams();
-  const titles = ['入库详情', '出库详情', '报废详情', '库存详情'];
+  const titles = ['入库详情', '出库详情', '报废详情', '库存详情', '基础库存'];
 
-  const params = {
+  const defaultParams = {
     type: Number(type),
     project_id: Number(projectId),
     check_result: 0,
@@ -22,27 +22,15 @@ const SparePartDetail = () => {
     end_time: '',
     name: '',
   };
-  const paramsExit = {
+
+  const defaultParamsOfExit = {
     category_id: 0,
     name: '',
-    warning_state: 0,
+    model_number: '',
+    warning_state: Number(type) === PageType.warning ? 2 : 0,
     project_id: Number(projectId),
   };
 
-  useEffect(() => {
-    type !== PageType.existing ? run(params) : runExist(paramsExit);
-  }, [type]);
-  const { data, run, loading } = useRequest((data) => queryStoreList(data), {
-    manual: true,
-  });
-
-  const {
-    data: dataExist,
-    loading: loadingExist,
-    run: runExist,
-  } = useRequest((data) => queryInventoryList(data), {
-    manual: true,
-  });
   const columns = [
     {
       title: '仓储类型',
@@ -63,6 +51,7 @@ const SparePartDetail = () => {
       align: 'center',
     },
   ];
+
   const columnsIn = [
     ...columns,
     {
@@ -91,6 +80,7 @@ const SparePartDetail = () => {
       align: 'center',
     },
   ];
+
   const columnsOut = [
     ...columns,
     {
@@ -125,6 +115,7 @@ const SparePartDetail = () => {
       align: 'center',
     },
   ];
+
   const columnsScrap = [
     ...columns,
     {
@@ -188,6 +179,7 @@ const SparePartDetail = () => {
       },
     },
   ];
+
   const columnsExit = [
     {
       title: '类型',
@@ -210,12 +202,17 @@ const SparePartDetail = () => {
     {
       title: '在库',
       align: 'center',
-      render: (record) => (
-        <div>
-          {record.in_amount - record.out_amount - record.scrap_amount}
-          {record.unit}
-        </div>
-      ),
+      render: (record) => {
+        return (
+          <div>
+            {record.in_amount -
+              record.out_amount -
+              record.scrap_amount +
+              record.update_amount}
+            {record.unit}
+          </div>
+        );
+      },
     },
     {
       title: '入库',
@@ -267,28 +264,162 @@ const SparePartDetail = () => {
       align: 'center',
     },
   ];
-  const columnsList = [columnsIn, columnsOut, columnsScrap, columnsExit];
+
+  const columnsBase = [
+    {
+      title: '类型',
+      dataIndex: 'category_name',
+      key: 'category_name',
+      align: 'center',
+    },
+    {
+      title: '名称',
+      dataIndex: 'name',
+      key: 'name',
+      align: 'center',
+    },
+    {
+      title: '型号',
+      dataIndex: 'model_number',
+      key: 'model_number',
+      align: 'center',
+    },
+    {
+      title: '在库',
+      align: 'center',
+      render: (record) => {
+        return (
+          <div>
+            {record.in_amount -
+              record.out_amount -
+              record.scrap_amount +
+              record.update_amount}
+            {record.unit}
+          </div>
+        );
+      },
+    },
+    {
+      title: '入库',
+      align: 'center',
+      render: (recode) => (
+        <div>
+          {recode.in_amount}
+          {recode.unit}
+        </div>
+      ),
+    },
+    {
+      title: '出库',
+      align: 'center',
+      render: (recode) => (
+        <div>
+          {recode.out_amount}
+          {recode.unit}
+        </div>
+      ),
+    },
+    {
+      title: '报废',
+      align: 'center',
+      render: (recode) => (
+        <div>
+          {recode.scrap_amount}
+          {recode.unit}
+        </div>
+      ),
+    },
+    {
+      title: '预警状态',
+      dataIndex: 'warning_state',
+      align: 'warning_state',
+      align: 'center',
+      render: (warn) => {
+        return warn == 2 ? (
+          <div style={{ color: 'red' }}>报警</div>
+        ) : (
+          <div>正常</div>
+        );
+      },
+    },
+  ];
+
+  const columnsList = [
+    columnsIn,
+    columnsOut,
+    columnsScrap,
+    columnsExit,
+    columnsBase,
+  ];
+
+  const [dataSource, setDataSource] = useState([]);
+
+  const [pagination, setPagination] = useState({
+    current: 1,
+    total: 0,
+    pageSize: 20,
+  });
+
+  const isLoadAll = useRef(false);
+
+  const bottomAreaOfTable = useRef();
+
+  const { run, loading } = useRequest(
+    (data) => {
+      data.page = pagination?.current || defaultParams.currentPage;
+      data.page_size = pagination?.pageSize || defaultParams.pageSize;
+      return queryStoreList(data);
+    },
+    {
+      manual: true,
+      formatResult: (result) => {
+        setDataSource(result?.data);
+      },
+    },
+  );
+
+  const { loading: loadingExist, run: runExist } = useRequest(
+    (data) => {
+      data.page = pagination?.current || defaultParamsOfExit.currentPage;
+      data.page_size = pagination?.pageSize || defaultParamsOfExit.pageSize;
+      return queryInventoryList(data);
+    },
+    {
+      manual: true,
+      formatResult: (result) => {
+        setDataSource(result?.data);
+      },
+    },
+  );
+
+  useEffect(() => {
+    Number(type) !== PageType.warning && Number(type) !== PageType.base
+      ? run(defaultParams)
+      : runExist(defaultParamsOfExit);
+  }, [type]);
 
   return (
     <PageContent closeable={false}>
       <PageTitle children={titles[type]} returnable />
-      {type !== PageType.existing ? (
-        <Table
-          loading={loading}
-          style={{ marginTop: '10px' }}
-          dataSource={data}
-          columns={columnsList[type]}
-          pagination={false}
-        />
-      ) : (
-        <Table
-          loading={loadingExist}
-          style={{ marginTop: '10px' }}
-          dataSource={dataExist}
-          columns={columnsList[type]}
-          pagination={false}
-        />
-      )}
+      <div className="content-title" style={{ marginTop: '10px' }}>
+        {Number(type) !== PageType.warning && Number(type) !== PageType.base ? (
+          <Table
+            sticky={{ offsetHeader: 0 }}
+            loading={loading}
+            dataSource={dataSource}
+            columns={columnsList[type]}
+            pagination={false}
+          />
+        ) : (
+          <Table
+            sticky={{ offsetHeader: 0 }}
+            loading={loadingExist}
+            dataSource={dataSource}
+            columns={columnsList[type]}
+            pagination={false}
+          />
+        )}
+      </div>
     </PageContent>
   );
 };

+ 38 - 30
src/pages/DeviceManager/index.js

@@ -22,7 +22,8 @@ export const PageType = {
   in: 0, //入库管理
   out: 1, //出库管理
   scrap: 2, //报废处置
-  existing: 3, //基础库存
+  warning: 3, //报警数量
+  base: 4, //基础库存
 };
 
 const DeviceManager = () => {
@@ -236,7 +237,7 @@ const Device = ({ projectId }) => {
   };
 
   return (
-    <div className={styles.sparePart}>
+    <div className={`content-tab ${styles.sparePart}`}>
       <Spin spinning={loadingDevice}>
         <div className={`card-box ${styles.titleContent}`}>
           <img className={styles.img} src={deviceIcon} />
@@ -291,13 +292,14 @@ const Device = ({ projectId }) => {
 const SparePart = ({ projectId }) => {
   const navigate = useNavigate();
   const year = dayjs().format('YYYY');
-  const params = {
+  const currentMonth = dayjs().format('MM');
+  const defaultParams = {
     project_id: Number(projectId),
-    month: 0,
+    month: Number(currentMonth),
     year: Number(year),
   };
   //请求备品列表
-  const { data, loading } = useRequest(() => queryMainChartList(params));
+  const { data, loading } = useRequest(() => queryMainChartList(defaultParams));
 
   const changePage = (type) => {
     navigate(`/device/detail/${projectId}/${type}`);
@@ -313,6 +315,10 @@ const SparePart = ({ projectId }) => {
 
   const list = useMemo(() => {
     const result = [
+      {
+        title: '基础库存',
+        type: PageType.base,
+      },
       {
         title: '入库数量',
         type: PageType.in,
@@ -330,7 +336,7 @@ const SparePart = ({ projectId }) => {
       },
       {
         title: '报警数量',
-        type: PageType.existing,
+        type: PageType.warning,
         num: data?.warning_stat || 0,
       },
     ];
@@ -338,31 +344,33 @@ const SparePart = ({ projectId }) => {
   }, [data]);
   return (
     <Spin spinning={loading}>
-      <Space direction="vertical" size={16} className={styles.sparePart}>
-        <div className={`card-box ${styles.titleContent}`}>
-          <img className={styles.img} src={spareIcon} />
-          <div>
-            <div className={styles.num}>{data?.on_amount || 0}</div>
-            <div className={styles.text}>在库数量(个)</div>
-          </div>
-          <div
-            onClick={handleTotalPage}
-            className={styles.iconFundFilled}
-          ></div>
-        </div>
-        {list.map((item) => (
-          <div
-            className={`card-box ${styles.cardItem}`}
-            onClick={() => changePage(item.type)}
-          >
-            <span className={styles.spareText}>{item.title}</span>
-            <Space size={30}>
-              <span className={styles.spareText}>{item.num}个</span>
-              <RightOutlined style={{ fontSize: '28px' }} />
-            </Space>
+      <div className="content-tab">
+        <Space direction="vertical" size={16} className={styles.sparePart}>
+          <div className={`card-box ${styles.titleContent}`}>
+            <img className={styles.img} src={spareIcon} />
+            <div>
+              <div className={styles.num}>{data?.on_amount || 0}</div>
+              <div className={styles.text}>在库数量(个)</div>
+            </div>
+            <div
+              onClick={handleTotalPage}
+              className={styles.iconFundFilled}
+            ></div>
           </div>
-        ))}
-      </Space>
+          {list.map((item) => (
+            <div
+              className={`card-box ${styles.cardItem}`}
+              onClick={() => changePage(item.type)}
+            >
+              <span className={styles.spareText}>{item.title}</span>
+              <Space size={30}>
+                {/* <span className={styles.spareText}>{item.num}个</span> */}
+                <RightOutlined style={{ fontSize: '28px' }} />
+              </Space>
+            </div>
+          ))}
+        </Space>
+      </div>
     </Spin>
   );
 };

+ 62 - 60
src/pages/DeviceManager/storage.js

@@ -124,69 +124,71 @@ const StorageOverview = () => {
   return (
     <PageContent closeable={false}>
       <PageTitle children="备品统计" returnable />
-      <div className={`card-box ${styles.main}`}>
-        <Spin spinning={loading}>
-          <ModuleTitle title="物料种类库存占比" />
-          <div style={{ height: '330px' }}>
-            {data?.pieData && <PieChartModule data={data.pieData} />}
-          </div>
-          <div className={styles.thereItem}>
-            <ModuleTitle title="物料库存排名前十统计" />
-            <ul
-              style={{
-                height: '330px',
-                display: 'flex',
-                flexDirection: 'column',
-              }}
-            >
-              {data?.kind && data.kind.length != 0 ? (
-                data.kind
-                  ?.sort((a, b) => b.value - a.value)
-                  .map((item) => {
-                    return (
-                      <li
-                        className={styles.li}
-                        key={`kind_${item.item}`}
-                        style={{ flexGrow: 1 }}
-                      >
-                        <div
-                          style={{
-                            width: '80px',
-                            textAlign: 'right',
-                            fontSize: '18px',
-                            whiteSpace: 'nowrap',
-                          }}
-                        >
-                          {item.item}
-                        </div>
-                        <div
-                          className={styles.line}
-                          style={{
-                            width: `${(70 * item.value) / data?.maxKind}%`,
-                          }}
-                        />
-                        {item.value}
-                      </li>
-                    );
-                  })
-              ) : (
-                <Empty />
-              )}
-            </ul>
-          </div>
-          <div className={styles.thereItem}>
-            <ModuleTitle title="物料报废统计" />
+      <div className="content-title">
+        <div className={`card-box${styles.main}`}>
+          <Spin spinning={loading}>
+            <ModuleTitle title="物料种类库存占比" />
             <div style={{ height: '330px' }}>
-              {data?.radarData && <RadarChartModule {...data.radarData} />}
+              {data?.pieData && <PieChartModule data={data.pieData} />}
             </div>
-          </div>
-          <div className={styles.twoItem}>
-            <ModuleTitle title="出入库统计" />
-            <div style={{ height: '330px' }}>
-              {data?.barData && <BarChartModule {...data.barData} />}
+            <div className={styles.thereItem}>
+              <ModuleTitle title="物料库存排名前十统计" />
+              <ul
+                style={{
+                  height: '330px',
+                  display: 'flex',
+                  flexDirection: 'column',
+                }}
+              >
+                {data?.kind && data.kind.length != 0 ? (
+                  data.kind
+                    ?.sort((a, b) => b.value - a.value)
+                    .map((item) => {
+                      return (
+                        <li
+                          className={styles.li}
+                          key={`kind_${item.item}`}
+                          style={{ flexGrow: 1 }}
+                        >
+                          <div
+                            style={{
+                              width: '80px',
+                              textAlign: 'right',
+                              fontSize: '18px',
+                              whiteSpace: 'nowrap',
+                            }}
+                          >
+                            {item.item}
+                          </div>
+                          <div
+                            className={styles.line}
+                            style={{
+                              width: `${(70 * item.value) / data?.maxKind}%`,
+                            }}
+                          />
+                          {item.value}
+                        </li>
+                      );
+                    })
+                ) : (
+                  <Empty />
+                )}
+              </ul>
+            </div>
+            <div className={styles.thereItem}>
+              <ModuleTitle title="物料报废统计" />
+              <div style={{ height: '330px' }}>
+                {data?.radarData && <RadarChartModule {...data.radarData} />}
+              </div>
+            </div>
+            <div className={styles.twoItem}>
+              <ModuleTitle title="出入库统计" />
+              <div style={{ height: '330px' }}>
+                {data?.barData && <BarChartModule {...data.barData} />}
+              </div>
             </div>
-          </div>
-        </Spin>
+          </Spin>
+        </div>
       </div>
     </PageContent>
   );

+ 1 - 2
src/pages/EqSelfInspection/List/index.js

@@ -38,8 +38,7 @@ function List(props) {
   return (
     <PageContent closeable={false}>
       <PageTitle returnable>自检记录</PageTitle>
-
-      <div className={styles.list}>
+      <div className={`content-title ${styles.list}`}>
         {list.map((item) => (
           <div className={styles.item} onClick={() => goToDetail(item)}>
             {item.Status == 0 ? (

+ 1 - 1
src/pages/EqSelfInspection/ReportDetail.js

@@ -22,7 +22,7 @@ const ReportDetail = (props) => {
   return (
     <PageContent closeable={false}>
       <PageTitle returnable>自检报告</PageTitle>
-      <div style={{ marginTop: 20 }}>
+      <div className="content-title" style={{ marginTop: 20 }}>
         <Detail data={data} projectId={projectId} loading={loading} />
       </div>
     </PageContent>

+ 195 - 0
src/pages/EqSelfInspection/components/Detail.js

@@ -174,6 +174,16 @@ function Detail(props) {
             userList={userList}
             title={<ModuleTitle title="环境异常" />}
           ></ReportCom>
+          {/* 液位异常 */}
+          <LiquidLevelCom
+            sendMessageToUnity={sendMessageToUnity}
+            select={select}
+            allData={data?.FluidLevelList}
+            key="liquid"
+            type={'liquid'}
+            userList={userList}
+            title={<ModuleTitle title="液位异常" />}
+          />
 
           {/* 安防检测异常 */}
           <ReportDumCom
@@ -761,6 +771,191 @@ function ReportCom(props) {
   );
 }
 
+export function LiquidTable(props) {
+  const { onClickItem, data = {}, items, select, type } = props;
+  const { ProjectId, Id } = data;
+  const [loading, setLoading] = useState(false);
+  const [currentItem, setCurrentItem] = useState({});
+
+  const columns = [
+    {
+      title: '设备名称',
+      width: '25%',
+      dataIndex: 'device_name',
+    },
+    {
+      title: '液位数',
+      width: '20%',
+      dataIndex: 'origin_value',
+    },
+    {
+      title: '差值',
+      width: '16%',
+      dataIndex: 'value',
+    },
+    {
+      title: '设定值范围',
+      width: '30%',
+      render: (record) => (
+        <ThresholdDetail
+          current={record.value || 0}
+          data={{
+            JsonNumThreshold: record?.json_num_threshold,
+            Type: record.type || 2,
+          }}
+        />
+      ),
+    },
+    {
+      title: '状态',
+      width: '13%',
+      dataIndex: 'status',
+      render: (status) => {
+        switch (status) {
+          case -1:
+          case 0:
+            return (
+              <div>
+                <i
+                  className={styles.iconStatus}
+                  style={{ background: '#12CEB3' }}
+                ></i>
+                正常
+              </div>
+            );
+          case 1:
+            return (
+              <div>
+                <i
+                  className={styles.iconStatus}
+                  style={{ background: '#FFE26D' }}
+                ></i>
+                异常
+              </div>
+            );
+          case 2:
+            return (
+              <div>
+                <i
+                  className={styles.iconStatus}
+                  style={{ background: '#FFE26D' }}
+                ></i>
+                警告
+              </div>
+            );
+        }
+      },
+    },
+  ];
+  const handleClickItem = (data) => {
+    onClickItem(`DeviceTable-${data.device_code}`, {
+      type: data.status,
+      deviceCode: data.device_code,
+    });
+  };
+
+  return (
+    <Table
+      columns={columns}
+      dataSource={items}
+      rowKey="device_code"
+      locale={{
+        emptyText: <Empty />,
+      }}
+      pagination={false}
+    />
+  );
+}
+
+function LiquidLevelCom(props) {
+  const { sendMessageToUnity, select, allData = [], type, title } = props;
+  const [activeKey, setActiveKey] = useState('1');
+
+  const errorData = useMemo(() => {
+    const errorData = allData?.filter((item) => item.status);
+    return errorData;
+  }, [allData]);
+
+  const handleTabsChange = (activeKey) => {
+    setActiveKey(activeKey);
+  };
+  return (
+    <div className={styles.detailCard}>
+      <div className={styles.tableTop}>
+        {title}
+        <TabsContent
+          defaultActiveKey="1"
+          onChange={handleTabsChange}
+          small={true}
+          items={[
+            {
+              key: '1',
+              label: `异常/警告(${errorData?.length || 0})`,
+              children: <div></div>,
+            },
+            {
+              key: '2',
+              label: `全部(${allData?.length || 0})`,
+              children: <div></div>,
+            },
+          ]}
+        ></TabsContent>
+      </div>
+
+      {activeKey == '1' && (
+        <LiquidTable
+          onClickItem={sendMessageToUnity}
+          select={select}
+          items={errorData}
+          key={type}
+          type={type}
+        />
+      )}
+      {activeKey == '2' && (
+        <LiquidTable
+          onClickItem={sendMessageToUnity}
+          select={select}
+          items={allData}
+          key={type}
+          type={type}
+        />
+      )}
+    </div>
+    // <div className={styles.detailCard}>
+    //   <Tabs
+    //     defaultActiveKey="1"
+    //     tabBarExtraContent={<div className={styles.tabBarExtraContent}>{title} </div>}
+    //     onChange={handleTabsChange}
+    //   >
+    //     <Tabs.TabPane tab={`异常/警告(${errorData.length || 0})`} key="1">
+    //       {activeKey == '1' && (
+    //         <LiquidTable
+    //           onClickItem={sendMessageToUnity}
+    //           select={select}
+    //           items={errorData}
+    //           key={type}
+    //           // data={data}
+    //           type={type}
+    //         />
+    //       )}
+    //     </Tabs.TabPane>
+    //     <Tabs.TabPane tab={`全部(${allData.length || 0})`} key="2">
+    //       {activeKey == '2' && (
+    //         <LiquidTable
+    //           onClickItem={sendMessageToUnity}
+    //           select={select}
+    //           items={allData}
+    //           // data={data}
+    //           key={type}
+    //           type={type}
+    //         />
+    //       )}
+    //     </Tabs.TabPane>
+    //   </Tabs>
+    // </div>
+  );
+}
+
 function ReportDumCom(props) {
   const { data = [], title } = props;
   const columns = [

+ 142 - 96
src/pages/EqSelfInspection/index.js

@@ -1,5 +1,6 @@
 import PageContent from '@/components/PageContent';
 import PageTitle from '@/components/PageTitle';
+import { getMandateIDs } from '@/services/eqSelfInspection';
 import { GetTokenFromUrl, UnityAction } from '@/utils/utils';
 import { connect, history, useLocation, useParams } from '@umijs/max';
 import { Button, Form, Modal, Select, Spin, message } from 'antd';
@@ -73,6 +74,24 @@ const EqSelfInspection = (props) => {
     form.resetFields();
   };
 
+  const openTaskModal = async () => {
+    if (!autoReport.Id) {
+      return;
+    }
+    let mandateIDs = await getMandateIDs({
+      project_id: projectId,
+      id: autoReport.Id,
+    }).catch((err) => {
+      console.log(err);
+    });
+    if (mandateIDs?.length) {
+      mandateIDs = [...new Set(mandateIDs)];
+      UnityAction.sendMsg('OpenTaskModal', `mandate_id=${mandateIDs.join()}`);
+    } else {
+      message.info('未查询到相关任务');
+    }
+  };
+
   useEffect(() => {
     if (routeId) {
       dispatch({
@@ -111,108 +130,131 @@ const EqSelfInspection = (props) => {
     <PageContent>
       <PageTitle>系统自检</PageTitle>
       <Spin spinning={loading} tip="正在自检中...">
-        <div
-          className={`${styles.itemMain} card-box`}
-          style={{ padding: '15px 24px' }}
-        >
-          <div style={{ fontSize: 28, color: 'rgb(110, 110, 110)' }}>
-            自检间隔:{autoReport?.RouteInfo?.PlanDur}分钟
-          </div>
+        <div className="content-title">
+          <div
+            className={`${styles.itemMain} card-box`}
+            style={{ padding: '15px 24px', position: 'relative' }}
+          >
+            <div style={{ fontSize: 28, color: 'rgb(110, 110, 110)' }}>
+              自检间隔:{autoReport?.RouteInfo?.PlanDur}分钟
+            </div>
 
-          <div className={styles.icon}>
-            <div
-              className={styles.iconDiffFilled}
-              onClick={() => {
-                history.push(`/self-inspection/list/${projectId}`);
-              }}
-            ></div>
-            <div
-              className={styles.iconFundFilled}
+            <div className={styles.icon}>
+              <div
+                className={styles.iconDiffFilled}
+                onClick={() => {
+                  history.push(`/self-inspection/list/${projectId}`);
+                }}
+              ></div>
+              <div
+                className={styles.iconFundFilled}
+                onClick={() => {
+                  history.push(
+                    `/self-inspection/statistics/${projectId}?JWT-TOKEN=${GetTokenFromUrl()}`,
+                  );
+                }}
+              ></div>
+            </div>
+            <div className={styles.logoInfo}>
+              <div className={styles.logo} />
+              <div className={styles.logoTitle}>
+                <div>
+                  系统自检&nbsp;
+                  {autoReport?.Status == 0 ? (
+                    <span style={{ color: '#39CA74' }}>正常</span>
+                  ) : (
+                    <span style={{ color: 'rgba(254, 88, 80, 1)' }}>异常</span>
+                  )}
+                </div>
+              </div>
+              <div className={styles.logoTime}>
+                {autoReport?.CreatedTime
+                  ? dayjs(autoReport?.CreatedTime).format('YYYY-MM-DD HH:mm')
+                  : ''}
+              </div>
+              <div className={styles.insbtn}>
+                <Button type="primary" onClick={HandleInspection}>
+                  一键自检
+                </Button>
+              </div>
+              <div className={styles.logoFont}>系统自检中</div>
+            </div>
+          </div>
+          <Item name="设备自检" status={patrolStatus}></Item>
+          <Item name="工艺自检" status={faultAnalysisStatus}></Item>
+          <Item name="安全自检" status={secureStatus}>
+            {secureChildren?.map((item, index) => (
+              <WarningItem
+                key={index}
+                label={item.label}
+                status={item.status}
+              />
+            ))}
+          </Item>
+          <div
+            className={styles.btnContainer}
+            style={{
+              justifyContent:
+                autoReport?.Status == 0 ? 'center' : 'space-between',
+            }}
+          >
+            {autoReport?.Status != 0 && (
+              <Button
+                className={styles.reportBtn}
+                type="primary"
+                onClick={() => {
+                  openTaskModal();
+                }}
+              >
+                查看任务
+              </Button>
+            )}
+            <Button
+              className={styles.reportBtn}
+              type="primary"
               onClick={() => {
                 history.push(
-                  `/self-inspection/statistics/${projectId}?JWT-TOKEN=${GetTokenFromUrl()}`,
+                  `/self-inspection/detail/${projectId}/${
+                    autoReport?.Id
+                  }?JWT-TOKEN=${GetTokenFromUrl()}`,
                 );
               }}
-            ></div>
-          </div>
-          <div className={styles.logoInfo}>
-            <div className={styles.logo} />
-            <div className={styles.logoTitle}>
-              <div>
-                系统自检&nbsp;
-                {autoReport?.Status == 0 ? (
-                  <span style={{ color: '#39CA74' }}>正常</span>
-                ) : (
-                  <span style={{ color: 'rgba(254, 88, 80, 1)' }}>异常</span>
-                )}
-              </div>
-            </div>
-            <div className={styles.logoTime}>
-              {autoReport?.CreatedTime
-                ? dayjs(autoReport?.CreatedTime).format('YYYY-MM-DD HH:mm')
-                : ''}
-            </div>
-            <div className={styles.insbtn}>
-              <Button type="primary" onClick={HandleInspection}>
-                一键自检
-              </Button>
-            </div>
-            <div className={styles.logoFont}>
-              系统自检中
-            </div>
+            >
+              查看自检报告
+            </Button>
           </div>
-        </div>
-        <Item name="设备自检" status={patrolStatus}></Item>
-        <Item name="工艺自检" status={faultAnalysisStatus}></Item>
-        <Item name="安全自检" status={secureStatus}>
-          {secureChildren?.map((item) => (
-            <WarningItem label={item.label} status={item.status} />
-          ))}
-        </Item>
-        <Button
-          className={styles.reportBtn}
-          type="primary"
-          onClick={() => {
-            history.push(
-              `/self-inspection/detail/${projectId}/${
-                autoReport?.Id
-              }?JWT-TOKEN=${GetTokenFromUrl()}`,
-            );
-          }}
-        >
-          查看自检报告
-        </Button>
 
-        <Modal
-          title="选择巡检路线"
-          destroyOnClose
-          open={visible}
-          onOk={handleOk}
-          width="40%"
-          maskClosable={false}
-          onCancel={handleCancel}
-        >
-          <Form form={form} labelCol={{ span: 5 }} wrapperCol={{ span: 15 }}>
-            <Form.Item
-              label="巡检路线"
-              name="routeId"
-              rules={[
-                {
-                  required: true,
-                  message: '请选择巡检路线',
-                },
-              ]}
-            >
-              <Select placeholder="请选择巡检路线">
-                {patrolList?.map((item) => (
-                  <Option key={item.Id} value={item.Id}>
-                    {item?.Name}
-                  </Option>
-                ))}
-              </Select>
-            </Form.Item>
-          </Form>
-        </Modal>
+          <Modal
+            title="选择巡检路线"
+            destroyOnClose
+            open={visible}
+            onOk={handleOk}
+            width="40%"
+            maskClosable={false}
+            onCancel={handleCancel}
+          >
+            <Form form={form} labelCol={{ span: 5 }} wrapperCol={{ span: 15 }}>
+              <Form.Item
+                label="巡检路线"
+                name="routeId"
+                rules={[
+                  {
+                    required: true,
+                    message: '请选择巡检路线',
+                  },
+                ]}
+              >
+                <Select placeholder="请选择巡检路线">
+                  {patrolList?.map((item) => (
+                    <Option key={item.Id} value={item.Id}>
+                      {item?.Name}
+                    </Option>
+                  ))}
+                </Select>
+              </Form.Item>
+            </Form>
+          </Modal>
+        </div>
       </Spin>
     </PageContent>
   );
@@ -237,7 +279,11 @@ const Item = (props) => {
           </div>
         );
       case 2:
-        return;
+        return (
+          <div className={styles.right} style={{ color: '#FFE26D' }}>
+            警告
+          </div>
+        );
       // return <SyncOutlined className={styles.right} spin />;
       default:
         return <div className={`${styles.iconSuccess} ${styles.right}`}></div>;

+ 9 - 3
src/pages/EqSelfInspection/index.less

@@ -127,11 +127,17 @@
   background: url('@/assets/icon-success.png') no-repeat;
   background-size: 100% 100%;
 }
+
+.btnContainer {
+  display: flex;
+  justify-content: space-between;
+}
+
 .reportBtn {
-  margin-top: 40px;
-  width: 100%;
+  margin-top: 10px;
+  width: 45%;
   height: 86px;
-  display: block;
+  display: inline-block;
   font-size: 30px;
   font-weight: 400;
   color: #ffffff;

+ 0 - 0
src/pages/Home/backlog.js


+ 62 - 5
src/pages/Home/index.js

@@ -1,3 +1,4 @@
+import { getPendingList } from '@/services/message';
 import { getComparisonData } from '@/services/OperationManagement';
 import { queryConditionSnapshot } from '@/services/SmartOps';
 import { getToken, UnityAction } from '@/utils/utils';
@@ -18,7 +19,7 @@ const HomePage = (props) => {
   return (
     <div className={styles.content}>
       <LeftContent data={data} />
-      <CenterContent />
+      {/* <CenterContent /> */}
       <RightContent data={data} />
     </div>
   );
@@ -31,6 +32,7 @@ const LeftContent = (props) => {
       <SmartWork data={data} />
       <WaterAmt data={data} />
       <WaterQuality data={data} />
+      <Backlog />
     </div>
   );
 };
@@ -48,6 +50,7 @@ const RightContent = (props) => {
       <SelfInspection />
       <Electric data={data} />
       <Medicine />
+      <Scada />
     </div>
   );
 };
@@ -230,19 +233,73 @@ const Scada = () => {
         (item) =>
           `${domain}smart-water/scada/index.html#/3dview/${projectId}/${item.id}?JWT-TOKEN=${token}&hideTitle=true`,
       );
-      return urls.splice(0, 2);
+      return urls.splice(0, 1);
     },
   });
   return (
     <Box
-      big
       title="工艺监控"
       onClick={() => UnityAction.sendMsg('menuItem', '工艺监控')}
     >
-      <div className={styles.scadaContent}>
+      <div className={styles.scada}>
         {data?.map((url) => (
-          <iframe src={url} />
+          <iframe style={{ width: '450px', height: '270px' }} src={url} />
         ))}
+        <div className={styles.mask}></div>
+      </div>
+    </Box>
+  );
+};
+
+// 待办事项
+const Backlog = (props) => {
+  // const { data } = props;
+  const { projectId } = useParams();
+
+  const { data, loading } = useRequest(getPendingList, {
+    defaultParams: [{ project_id: projectId }],
+  });
+  const handleClick = (item) => {
+    if (item.type === 0) {
+      // task
+      UnityAction.sendMsg('OpenTaskModal', `mandate_id=${item.origin_id}`);
+    } else {
+      // order
+      UnityAction.sendMsg(
+        'OpenWorkOrderModal',
+        `order_id=${item.origin_id}&order_type=${item.origin_type}`,
+      );
+    }
+  };
+  return (
+    <Box
+      title={
+        <div style={{ display: 'flex' }}>
+          待办事项
+          {/* <div className={styles.count}>{data?.length || 0}</div> */}
+        </div>
+      }
+      onClick={() => UnityAction.sendMsg('menuItem', '待办事项')}
+    >
+      <div className={styles.backlog}>
+        <div>
+          {data?.map((item) => (
+            <>
+              <div className={styles.createTime}>{item.time}</div>
+              <div
+                className={styles.item}
+                onClick={(e) => {
+                  e.stopPropagation();
+                  handleClick(item);
+                }}
+              >
+                <div className={styles.point} />
+                <div className={styles.titleText}>{item.title}</div>
+                <div className={styles.bottomCon}>{item.content}</div>
+              </div>
+            </>
+          ))}
+        </div>
       </div>
     </Box>
   );

+ 58 - 5
src/pages/Home/index.less

@@ -4,7 +4,7 @@
   align-items: center;
   height: 100vh;
   width: 100%;
-  padding-top: 300px;
+  padding-top: 90px;
 }
 .box {
   background: url('@/assets/home-box-bg.png') no-repeat center;
@@ -12,7 +12,7 @@
   width: 520px;
   padding: 24px;
   position: relative;
-  margin-bottom: 40px;
+  margin-bottom: 20px;
   cursor: pointer;
   color: #fff;
 
@@ -21,7 +21,7 @@
   }
 
   .boxTitle {
-    padding-left: 68px;
+    padding-left: 46px;
     margin-bottom: 20px;
     font-size: 26px;
     font-weight: 400;
@@ -53,7 +53,18 @@
 }
 .boxBig {
   .box;
-  background-image:  url('@/assets/home-box-bg2.png');
+  background-image: url('@/assets/home-box-bg2.png');
+}
+.scada {
+  padding: 0 10px;
+  position: relative;
+  .mask {
+    position: absolute;
+    top: 0;
+    left: 0;
+    width: 100%;
+    height: 100%;
+  }
 }
 .scadaContent {
   padding: 20px;
@@ -121,4 +132,46 @@
   .box {
     width: 100%;
   }
-}
+}
+.backlog {
+  padding: 0 10px;
+  overflow-y: auto;
+  height: 200px;
+
+  .createTime {
+  }
+  .item {
+    margin: 8px 0 20px;
+    width: 100%;
+    font-size: 22px;
+    // height: 50px;
+    background-color: #346194;
+    line-height: 50px;
+  }
+  .point {
+    position: relative;
+    float: left;
+    top: 20px;
+    margin: 0 10px;
+    display: inline-block;
+    width: 10px;
+    height: 10px;
+    border-radius: 50%;
+    background-color: yellow;
+  }
+  .titleText {
+    margin: 0 20px;
+    border-bottom: 1px solid #eeeeee;
+  }
+  .bottomCon {
+    padding: 0 20px;
+    font-size: 20px;
+    word-wrap: break-word;
+  }
+}
+.count {
+  background: #f69040;
+  border-radius: 4px;
+  margin-left: 10px;
+  padding: 0 6px;
+}

+ 14 - 11
src/pages/Menu/index.js

@@ -1,39 +1,42 @@
-import { useParams } from '@umijs/max';
+import { UnityAction } from '@/utils/utils';
 import { useState } from 'react';
 import styles from './index.less';
-import { UnityAction } from '@/utils/utils';
 
 const menuList = [
   {
     name: '首页',
-    path: (projectId) => `/home/${projectId}`,
+    // path: (projectId) => `/home/${projectId}`,
   },
   {
     name: '工况管理',
-    path: (projectId) => `/smart/work/${projectId}`,
+    // path: (projectId) => `/smart/work/${projectId}`,
   },
   {
     name: '系统自检',
-    path: (projectId) => `/self-inspection/${projectId}`,
+    // path: (projectId) => `/self-inspection/${projectId}`,
   },
   {
     name: '智能管控',
-    path: (projectId) => `/hardware-controller/${projectId}`,
+    // path: (projectId) => `/hardware-controller/${projectId}`,
   },
   {
     name: '数字孪生',
   },
+  {
+    name: '智慧运营',
+    // path: (projectId) => `/smart-ops/${projectId}`,
+  },
   {
     name: '安全管理',
-    path: (projectId) => `/safety/${projectId}`,
+    // path: (projectId) => `/safety/${projectId}`,
   },
   {
     name: '任务管理',
-    path: (projectId) => `/task-manage/${projectId}`,
+    // path: (projectId) => `/task-manage/${projectId}`,
   },
   {
     name: '设备管理',
-    path: (projectId) => `/device/${projectId}`,
+    // path: (projectId) => `/device/${projectId}`,
   },
 ];
 
@@ -41,8 +44,8 @@ function Menu() {
   const [active, setActive] = useState('首页');
   const handleClick = (item) => {
     setActive(item.name);
-    
-    UnityAction.sendMsg("menuItem", item.name)
+
+    UnityAction.sendMsg('menuItem', item.name);
   };
 
   return (

+ 1 - 1
src/pages/MessageCenter/index.less

@@ -45,7 +45,7 @@
 }
 .allRead {
   position: absolute;
-  top: 68px;
+  top: 47.5px;
   line-height: 24px;
   right: 104px;
   font-size: 32px;

+ 14 - 46
src/pages/SafetyManagement/doorDetail.js

@@ -1,22 +1,18 @@
 import PageContent from '@/components/PageContent';
 import PageTitle from '@/components/PageTitle';
+import ScrollLoading from '@/components/ScrollLoading';
 import { queryGateOpList } from '@/services/safety';
 import { useParams, useRequest } from '@umijs/max';
 import { Table } from 'antd';
-import { useEffect, useRef, useState } from 'react';
+import { useState } from 'react';
 const DoorDetail = () => {
   const { projectId } = useParams();
   const [list, setList] = useState([]);
   const [curPagination, setCurPagination] = useState({});
 
-  const bottomAreaOfList = useRef();
-  // 是否已经发送请求 防止发送多次同样的请求
-  const load = useRef();
-
   const { run, loading } = useRequest((data) => queryGateOpList(data), {
     defaultParams: [{ project_id: projectId }],
     onSuccess: (data) => {
-      load.current = false;
       setList([...list, ...data.list]);
       setCurPagination(data.pagination);
     },
@@ -65,51 +61,23 @@ const DoorDetail = () => {
     },
   ];
 
-  const handleScroll = () => {
-    const { current, total } = curPagination;
-    if (list.length >= total) {
-      return;
-    }
-    const rect = bottomAreaOfList.current.getBoundingClientRect();
-    const isVisible =
-      rect.top >= 0 &&
-      rect.left >= 0 &&
-      rect.bottom <=
-        (window.innerHeight || document.documentElement.clientHeight) &&
-      rect.right <= (window.innerWidth || document.documentElement.clientWidth);
-
-    if (isVisible && !load.current) {
-      load.current = true;
-      run({ project_id: projectId, current: current + 1 });
-    }
-  };
-
-  // 监听滚动事件
-  useEffect(() => {
-    window.addEventListener('scroll', handleScroll);
-    return () => {
-      window.removeEventListener('scroll', handleScroll);
-    };
-  }, [loading]);
-
   return (
     <PageContent closeable={false}>
       <PageTitle children="门禁日志" returnable />
 
-      <Table
+      <ScrollLoading
         loading={loading}
-        style={{ marginTop: '30px' }}
-        dataSource={list}
-        columns={columns}
-        pagination={false}
-      />
-      <div
-        ref={bottomAreaOfList}
-        style={{
-          height: '1px',
-        }}
-      />
-      <div />
+        pagination={curPagination}
+        handleLoadData={(current) => run({ project_id: projectId, current })}
+      >
+        <Table
+          loading={loading}
+          style={{ marginTop: '30px' }}
+          dataSource={list}
+          columns={columns}
+          pagination={false}
+        />
+      </ScrollLoading>
     </PageContent>
   );
 };

+ 69 - 61
src/pages/SafetyManagement/index.js

@@ -118,37 +118,39 @@ const Video = ({ data, dataOnline, loading }) => {
   };
   return (
     <Spin spinning={loading}>
-      <Space direction="vertical" size={16} className={styles.sparePart}>
-        <div className={`card-box ${styles.titleContent}`}>
-          <div className={styles.titleLeft}>
-            <img className={styles.img} src={videoIcon} />
-            <div className={styles.textCon}>
-              <div className={styles.num}>{dataOnline?.total || 0}</div>
-              <div className={styles.text}>在库数量(个)</div>
-            </div>
-          </div>
-          <div>
-            <div className={styles.lTextCon1}>
-              <div className={styles.onlinePoint} />
-              <span className={styles.text}>
-                在线:{dataOnline?.online || 0}
-              </span>
+      <div className="content-tab">
+        <Space direction="vertical" size={16} className={styles.sparePart}>
+          <div className={`card-box ${styles.titleContent}`}>
+            <div className={styles.titleLeft}>
+              <img className={styles.img} src={videoIcon} />
+              <div className={styles.textCon}>
+                <div className={styles.num}>{dataOnline?.total || 0}</div>
+                <div className={styles.text}>在库数量(个)</div>
+              </div>
             </div>
-            <div className={styles.lTextCon2}>
-              <div className={styles.outlinePoint} />
-              <span className={styles.text}>
-                离线:{dataOnline?.offline || 0}
-              </span>
+            <div>
+              <div className={styles.lTextCon1}>
+                <div className={styles.onlinePoint} />
+                <span className={styles.text}>
+                  在线:{dataOnline?.online || 0}
+                </span>
+              </div>
+              <div className={styles.lTextCon2}>
+                <div className={styles.outlinePoint} />
+                <span className={styles.text}>
+                  离线:{dataOnline?.offline || 0}
+                </span>
+              </div>
             </div>
           </div>
-        </div>
 
-        {data?.map((item, idx) => (
-          <div key={`video_${idx}`} className="card-box">
-            {renderRed(item)}
-          </div>
-        ))}
-      </Space>
+          {data?.map((item, idx) => (
+            <div key={`video_${idx}`} className="card-box">
+              {renderRed(item)}
+            </div>
+          ))}
+        </Space>
+      </div>
     </Spin>
   );
 };
@@ -172,47 +174,53 @@ const Door = ({ data, dataOver, loading, projectId }) => {
   };
   return (
     <Spin spinning={loading}>
-      <Space direction="vertical" size={16} className={styles.doorPart}>
-        <div className={styles.titleContent}>
-          <div className={`card-box ${styles.cardLeft}`}>
-            <div className={styles.up}>
-              <img className={styles.img} src={doorIcon} />
-              <div className={styles.textCon}>
-                <div className={styles.num}>{dataOver?.total}</div>
-                <div className={styles.text}>门禁数量(个)</div>
+      <div className="content-tab">
+        <Space direction="vertical" size={16} className={styles.doorPart}>
+          <div className={styles.titleContent}>
+            <div className={`card-box ${styles.cardLeft}`}>
+              <div className={styles.up}>
+                <img className={styles.img} src={doorIcon} />
+                <div className={styles.textCon}>
+                  <div className={styles.num}>{dataOver?.total}</div>
+                  <div className={styles.text}>门禁数量(个)</div>
+                </div>
+              </div>
+              <div className={styles.bottom}>
+                <div className={styles.lTextCon2}>
+                  <div className={styles.onlinePoint} />
+                  <span className={styles.text}>在线:{dataOver?.online}</span>
+                </div>
+                <div className={styles.lTextCon2}>
+                  <div className={styles.outlinePoint} />
+                  <span className={styles.text}>离线:{dataOver?.offline}</span>
+                </div>
               </div>
             </div>
-            <div className={styles.bottom}>
-              <div className={styles.lTextCon2}>
-                <div className={styles.onlinePoint} />
-                <span className={styles.text}>在线:{dataOver?.online}</span>
+            <div className={`card-box ${styles.cardRight}`}>
+              <div className={styles.textContent}>
+                <div className={styles.in} />
+                <span className={styles.text}>
+                  今日进厂次数:{dataOver?.today_in}
+                </span>
               </div>
-              <div className={styles.lTextCon2}>
-                <div className={styles.outlinePoint} />
-                <span className={styles.text}>离线:{dataOver?.offline}</span>
+              <div className={styles.textContent}>
+                <div className={styles.out} />
+                <span className={styles.text}>
+                  今日出厂次数:{dataOver?.today_out}
+                </span>
               </div>
+              <Button className={styles.btn} onClick={handleClick}>
+                门禁日志
+              </Button>
             </div>
           </div>
-          <div className={`card-box ${styles.cardRight}`}>
-            <div className={styles.textContent}>
-              <div className={styles.in} />
-              <span className={styles.text}>今日进厂人数:{2}</span>
-            </div>
-            <div className={styles.textContent}>
-              <div className={styles.out} />
-              <span className={styles.text}>今日出厂人数:{2}</span>
+          {data?.map((item, idx) => (
+            <div key={`door_${idx}`} className="card-box">
+              {renderRed(item)}
             </div>
-            <Button className={styles.btn} onClick={handleClick}>
-              门禁日志
-            </Button>
-          </div>
-        </div>
-        {data?.map((item, idx) => (
-          <div key={`door_${idx}`} className="card-box">
-            {renderRed(item)}
-          </div>
-        ))}
-      </Space>
+          ))}
+        </Space>
+      </div>
     </Spin>
   );
 };

+ 66 - 64
src/pages/Smart/ConditionDetection.js

@@ -48,68 +48,71 @@ const ConditionDetection = (props) => {
   return (
     <PageContent closeable={false}>
       <PageTitle returnable>工况检测</PageTitle>
-      <div className={styles.circle}>
-        <CircleScore big>
-          <span className={styles.circleText}>{score}</span>
-        </CircleScore>
-        {/* <p>{desc}</p> */}
-        <p>膜车间当前运行状态{grade}</p>
-      </div>
-      <div className={styles.content}>
-        <Row gutter={16}>
-          <Col span={12} style={{ padding: 22 }}>
-            <div className={`${styles.card} card-box`}>
-              <h3>
-                实时工况 <span>{real.score}分</span>
-              </h3>
-              <ul>
-                <li>
-                  <i></i>水质达标率评分:{real.water}
-                </li>
-                <li>
-                  <i></i>能耗评分:{real.energy}
-                </li>
-                <li>
-                  <i></i>药耗评分:{real.medicine}
-                </li>
-                <li>
-                  <i></i>设施设备利用率评分:{real.device_rate}
-                </li>
-                <li>
-                  <i></i>设施设备完好率评分:{real.device_intact}
-                </li>
-              </ul>
-            </div>
-          </Col>
-          <Col span={12} style={{ padding: 22 }}>
-            <div className={`${styles.card2} card-box`}>
-              <h3>
-                目标工况 <span>{best.score}分</span>
-              </h3>
-              <ul>
-                <li>
-                  <i></i>水质达标率评分:{best.water}
-                </li>
-                <li>
-                  <i></i>能耗评分:{best.energy}
-                </li>
-                <li>
-                  <i></i>药耗评分:{best.medicine}
-                </li>
-                <li>
-                  <i></i>设施设备利用率评分:{best.device_rate}
-                </li>
-                <li>
-                  <i></i>设施设备完好率评分:{best.device_intact}
-                </li>
-              </ul>
-            </div>
-          </Col>
-        </Row>
-        <div className={styles.img}></div>
-      </div>
 
-      <ChartContent projectId={pid} />
+      <div className="content-title">
+        <div className={styles.circle}>
+          <CircleScore big>
+            <span className={styles.circleText}>{score}</span>
+          </CircleScore>
+          {/* <p>{desc}</p> */}
+          <p>膜车间当前运行状态{grade}</p>
+        </div>
+        <div className={styles.content}>
+          <Row gutter={16}>
+            <Col span={12} style={{ padding: 22 }}>
+              <div className={`${styles.card} card-box`}>
+                <h3>
+                  实时工况 <span>{real.score}分</span>
+                </h3>
+                <ul>
+                  <li>
+                    <i></i>水质达标率评分:{real.water}
+                  </li>
+                  <li>
+                    <i></i>能耗评分:{real.energy}
+                  </li>
+                  <li>
+                    <i></i>药耗评分:{real.medicine}
+                  </li>
+                  <li>
+                    <i></i>设施设备利用率评分:{real.device_rate}
+                  </li>
+                  <li>
+                    <i></i>设施设备完好率评分:{real.device_intact}
+                  </li>
+                </ul>
+              </div>
+            </Col>
+            <Col span={12} style={{ padding: 22 }}>
+              <div className={`${styles.card2} card-box`}>
+                <h3>
+                  目标工况 <span>{best.score}分</span>
+                </h3>
+                <ul>
+                  <li>
+                    <i></i>水质达标率评分:{best.water}
+                  </li>
+                  <li>
+                    <i></i>能耗评分:{best.energy}
+                  </li>
+                  <li>
+                    <i></i>药耗评分:{best.medicine}
+                  </li>
+                  <li>
+                    <i></i>设施设备利用率评分:{best.device_rate}
+                  </li>
+                  <li>
+                    <i></i>设施设备完好率评分:{best.device_intact}
+                  </li>
+                </ul>
+              </div>
+            </Col>
+          </Row>
+          <div className={styles.img}></div>
+        </div>
+
+        <ChartContent projectId={pid} />
+      </div>
     </PageContent>
   );
 };
@@ -150,7 +153,7 @@ const ChartContent = ({ projectId }) => {
       tooltip: {
         trigger: 'axis',
         textStyle: {
-          fontSize: 24
+          fontSize: 24,
         },
       },
       legend: {
@@ -168,14 +171,13 @@ const ChartContent = ({ projectId }) => {
       xAxis: {
         type: 'category',
         boundaryGap: false,
-        data: data[0].list?.map((item) => item.name.split(" ")[1]),
+        data: data[0].list?.map((item) => item.name.split(' ')[1]),
         nameTextStyle: {
           fontSize: 24,
         },
         axisLabel: {
           fontSize: 24,
         },
-       
       },
       yAxis: {
         type: 'value',

+ 62 - 67
src/pages/Smart/OptimizationTasks.js

@@ -5,72 +5,42 @@ import {
   queryMandate,
   queryMandateChildList,
   querySimulationProfit,
-  queryUserList,
 } from '@/services/SmartOps';
-import { history, useLocation, useParams, useRequest } from '@umijs/max';
-import { Col, Row, Table } from 'antd';
+import { UnityAction } from '@/utils/utils';
+import { useParams, useRequest } from '@umijs/max';
+import { Table } from 'antd';
 import dayjs from 'dayjs';
-import { useMemo } from 'react';
+import { useState } from 'react';
 import styles from './OptimizationTasks.less';
 
 const OptimizationTasks = (props) => {
   const { projectId } = useParams();
-  const { score } = useLocation();
 
   const { data, run } = useRequest(queryMandate, {
     manual: true,
   });
-  const { data: userList } = useRequest(queryUserList, {
-    defaultParams: [{ projectId }],
-  });
-
-  const ResponsiblePeople = useMemo(() => {
-    if (!data || !userList) return '-';
-    let user = userList.find((item) => item.ID == data.ResponsiblePeople);
-    return user?.CName || '-';
-  }, [data, userList]);
-
-  const status = { 0: '未处理', 2: '已完成', 4: '已忽略', 5: '已派遣' };
 
   return (
     <PageContent closeable={false}>
-      <PageTitle returnable>优化任务</PageTitle>
-      <div className={styles.pageCard}>
-        <div style={{ padding: 20 }}>
-          <Produce
-            projectId={projectId}
-            score={score}
-            queryMandate={run}
-            mandate={data}
-          />
+      <PageTitle returnable>
+        <div className={styles.navigateTitle}>
+          <div>优化任务</div>
+          <div className={styles.titleTime}>
+            {dayjs().format('MM月DD日 HH:mm')}
+          </div>
+        </div>
+      </PageTitle>
+      <div className="content-title">
+        <div>
+          <Produce projectId={projectId} />
           <Cost projectId={projectId} />
         </div>
-
-        {data && (
-          <div className={styles.bottom}>
-            <Row gutter={16}>
-              <Col span={8}>
-                <div className={styles.bottomText}>任务类型:系统发送</div>
-              </Col>
-              <Col span={8}>
-                <div className={styles.bottomText}>
-                  任务负责人:{ResponsiblePeople}
-                </div>
-              </Col>
-              <Col span={8}>
-                <div className={styles.bottomText}>
-                  任务状态:{status[data?.Status]}
-                </div>
-              </Col>
-            </Row>
-          </div>
-        )}
       </div>
     </PageContent>
   );
 };
 
-const Produce = ({ projectId, queryMandate }) => {
+const Produce = ({ projectId }) => {
   const columns = [
     {
       title: '参数',
@@ -82,6 +52,8 @@ const Produce = ({ projectId, queryMandate }) => {
     },
   ];
 
+  const [mandateID, setMandateID] = useState();
+
   const { data } = useRequest(queryMandateChildList, {
     defaultParams: [
       {
@@ -89,31 +61,41 @@ const Produce = ({ projectId, queryMandate }) => {
         mandateClass: 1,
       },
     ],
-    onSuccess(data) {
-      if (data.length > 0) {
-        queryMandate({
-          mandate_id: data[0]?.MandateId,
-        });
-      }
+    onSuccess: (data) => {
+      setMandateID(data[0].MandateId);
     },
   });
 
+  const openDetail = () => {
+    // send message to unity with mandate_id
+    if (!mandateID) {
+      return;
+    }
+    UnityAction.sendMsg('OpenTaskModal', `mandate_id=${mandateID}`);
+  };
+
   return (
-    <div style={{ marginBottom: 30 }}>
-      <h3 className={styles.title}>
-        <i />
-        生产调度类
+    <div className={styles.pageCard}>
+      <h3 className={styles.title} style={{ justifyContent: 'space-between' }}>
+        <div>
+          <i />
+          生产调度类
+        </div>
+        {mandateID && (
+          <div className={styles.btnBlue} onClick={openDetail}>
+            查看任务
+          </div>
+        )}
       </h3>
       {data?.length > 0 && (
         <>
-          <div className={styles.orderIcon}>任务已发送</div>
           <div
             className={styles.content}
             style={{ backgroundColor: '#B1D2F3' }}
           >
             <h3 className={styles.left}>任务总结</h3>
             <div className={styles.desc}>
-              根据水质相关数.建议您调节以下参数,水厂运行可达较优状态
+              根据水质相关数.建议您调节以下参数,水厂运行可达较优状态
             </div>
           </div>
           <div
@@ -133,7 +115,7 @@ const Produce = ({ projectId, queryMandate }) => {
       {!data?.length && (
         <div className={styles.content} style={{ backgroundColor: '#B1D2F3' }}>
           <div className={styles.desc}>
-            当前进水数据稳定,产水数据稳定,暂无调节任务,继续保持哦~
+            当前进水数据稳定,产水数据稳定,暂无调节任务,继续保持哦~
           </div>
         </div>
       )}
@@ -153,6 +135,8 @@ const Cost = ({ projectId }) => {
     },
   ];
 
+  const [mandateID, setMandateID] = useState();
+
   const { data: profit } = useRequest(querySimulationProfit, {
     defaultParams: [
       {
@@ -163,13 +147,13 @@ const Cost = ({ projectId }) => {
     ],
     formatResult(data) {
       if (!data?.info) return '-';
-
       return Object.values(data.info).reduce(
         (total, currentValue) => total + currentValue,
         0,
       );
     },
   });
+
   const { data } = useRequest(queryMandateChildList, {
     defaultParams: [
       {
@@ -177,20 +161,31 @@ const Cost = ({ projectId }) => {
         mandateClass: 2,
       },
     ],
+    onSuccess: (data) => {
+      setMandateID(data[0].MandateId);
+    },
   });
+
+  const openDetail = () => {
+    // send message to unity
+    if (!mandateID) {
+      return;
+    }
+    UnityAction.sendMsg('OpenTaskModal', `mandate_id=${mandateID}`);
+  };
+
   return (
-    <div>
+    <div className={styles.pageCard}>
       <h3 className={styles.title} style={{ justifyContent: 'space-between' }}>
         <div>
           <i style={{ background: '#F5A623' }} />
           成本节约类
         </div>
-        <div
-          className={styles.btn}
-          onClick={() => history.push(`/smart/simulate/${projectId}`)}
-        >
-          模拟评估
-        </div>
+        {mandateID && (
+          <div className={styles.btnOrange} onClick={openDetail}>
+            查看任务
+          </div>
+        )}
       </h3>
       {data?.length > 0 && (
         <>

+ 29 - 2
src/pages/Smart/OptimizationTasks.less

@@ -1,3 +1,17 @@
+.navigateTitle {
+  width: 100%;
+  display: flex;
+  flex-direction: row;
+  justify-content: space-between;
+  align-items: center;
+  .titleTime {
+    height: 50px;
+    font-size: 26px;
+    line-height: 26px;
+    display: flex;
+    align-items: flex-end;
+  }
+}
 .title {
   font-size: 32px;
   margin-bottom: 16px;
@@ -49,12 +63,12 @@
   color: #fff;
 }
 .pageCard {
+  padding: 20px;
   margin-top: 30px;
   overflow: hidden;
   border-radius: 26px;
   box-shadow: 2px 0 8px 0 rgba(0, 0, 0, 0.3);
   border: 1px solid #eee;
-  position: relative;
   background: rgb(255, 255, 255);
 }
 .bottom {
@@ -69,7 +83,7 @@
     text-align: center;
   }
 }
-.btn {
+.btnOrange {
   width: 120px;
   height: 56px;
   background: rgba(245, 166, 35, 0.75);
@@ -80,6 +94,19 @@
   line-height: 56px;
   text-align: center;
 }
+
+.btnBlue {
+  width: 120px;
+  height: 56px;
+  background: rgba(74, 144, 226, 0.75);
+  border-radius: 5px;
+  font-size: 24px;
+  font-weight: 400;
+  color: #ffffff;
+  line-height: 56px;
+  text-align: center;
+}
+
 .table2 {
   :global {
     .ant-table-thead {

+ 2 - 2
src/pages/Smart/components/SimulatePie.js

@@ -13,8 +13,8 @@ const SimulatePie = (props) => {
     defaultParams: [
       {
         project_id: projectId,
-        s_time: dayjs().subtract(1, 'day').format('YYYY-MM-DD HH:mm:ss'),
-        e_time: dayjs().format('YYYY-MM-DD HH:mm:ss'),
+        s_time: dayjs().subtract(1, 'day').format('YYYY-MM-DD HH:00:00'),
+        e_time: dayjs().format('YYYY-MM-DD HH:00:00'),
       },
     ],
     onSuccess(data) {

+ 4 - 2
src/pages/Smart/index.js

@@ -20,6 +20,7 @@ const Work = (props) => {
   return (
     <PageContent>
       <PageTitle>水厂工况</PageTitle>
+      {/* <div className="content-title"> */}
       <div className={styles.score}>
         <CircleScore big>
           {data?.score}
@@ -138,7 +139,7 @@ const Work = (props) => {
       </Row>
       <Row gutter={16}>
         <Col span={12}>
-          <div className={styles.card2}>
+          <div className={styles.card2} style={{ marginBottom: 0 }}>
             <h3>能耗数据</h3>
             <ul>
               <li>
@@ -151,7 +152,7 @@ const Work = (props) => {
           </div>
         </Col>
         <Col span={12}>
-          <div className={styles.card2}>
+          <div className={styles.card2} style={{ marginBottom: 0 }}>
             <h3>药耗数据</h3>
             <ul>
               <li>
@@ -164,6 +165,7 @@ const Work = (props) => {
           </div>
         </Col>
       </Row>
+      {/* </div> */}
     </PageContent>
   );
 };

+ 0 - 1
src/pages/Smart/index.less

@@ -76,7 +76,6 @@
 
 .card2 {
   .card;
-
   i {
     background-color: #ffc800;
   }

+ 204 - 0
src/pages/SmartOps/Analysis.js

@@ -0,0 +1,204 @@
+import TabsContent from '@/components/TabsContent';
+import { UnityAction } from '@/utils/utils';
+import { connect } from '@umijs/max';
+import { Button, Spin, Table, Tabs } from 'antd';
+import { useEffect, useMemo, useState } from 'react';
+import ThresholdBar from './components/ThresholdBar';
+import styles from './index.less';
+const { TabPane } = Tabs;
+
+const Analysis = (props) => {
+  const Status = [
+    { name: '异常', type: '1', data: [] },
+    { name: '警告', type: '2', data: [] },
+    { name: '提醒', type: '3', data: [] },
+  ];
+
+  const { list = [], processList, loading } = props;
+  const [selectedRowKeys, setSelectedRowKeys] = useState();
+  const [tab, setTab] = useState('1');
+
+  const columns = [
+    {
+      title: '设备名称',
+      width: '20%',
+      render: (record) => (
+        <div>
+          {record.DeviceName}({record.DeviceCode})
+        </div>
+      ),
+    },
+    {
+      title: '工艺单元',
+      width: '14%',
+      dataIndex: 'ProcessSectionId',
+      render: (id) => {
+        return processList.find((item) => item.id == id)?.name;
+      },
+    },
+    {
+      title: '阈值范围',
+      width: '14%',
+      render: (record) => {
+        // let thresholds = [-3,4,6,1];
+        let thresholds = record.fault_range.split(',');
+        return (
+          <div>
+            {record.fault_range && (
+              <ThresholdBar
+                current={record.fault_result}
+                thresholds={thresholds}
+              />
+            )}
+            {record.plcs && (
+              <Button
+                type="primary"
+                className={styles.anaBtn}
+                onClick={() =>
+                  handleBtnClick(
+                    record.plcs,
+                    record.DeviceName || record.DeviceCode,
+                  )
+                }
+              >
+                相关性分析
+              </Button>
+            )}
+          </div>
+        );
+      },
+    },
+    {
+      title: '可能原因',
+      width: '20%',
+      dataIndex: 'Reason',
+    },
+    {
+      title: '解决方案',
+      render: (record) => {
+        if (record.FixPlan instanceof Array) {
+          return (
+            <div>
+              {record.FixPlan.map((item) => (
+                <div>
+                  {item.content}
+                  <br />
+                </div>
+              ))}
+            </div>
+          );
+        } else {
+          return record.FixPlan;
+        }
+      },
+    },
+  ];
+
+  useEffect(() => {
+    UnityAction.on('SynDev', selectedItem);
+    return () => UnityAction.off('SynDev', selectedItem);
+  }, [data, tab]);
+
+  const selectedItem = (e) => {
+    setSelectedRowKeys(e);
+    console.log(data);
+    // alert(e)
+    const itemIndex = data?.findIndex((item) => item.type == tab);
+    const item = data[itemIndex]?.data?.find(
+      (item) => item.DeviceCode == e,
+    );
+    if (item) {
+      const dom = document.querySelector(`tr[data-row-key="${item.index}"]`);
+      if (dom) {
+        let v = document.getElementsByClassName('ant-table-body')[itemIndex];
+        v.scrollTop = dom.offsetTop;
+      }
+    }
+  };
+
+  const data = useMemo(() => {
+    return Status.map((item, idx) => {
+      const data = list?.filter((cur) => cur.grade_alarm == item.type);
+      return { ...item, data };
+    });
+  }, [list]);
+
+  const handleBtnClick = (plcs, title) => {
+    if (!plcs) return;
+    const msg = JSON.stringify(plcs); //[{ dataitemid: 'Raw_Water_Tank_Level', deviceid: '1436022785' }]
+    // router.push(`/unity/smart-ops/chart-page/46?type=2&data=${msg}`);
+    UnityAction.sendMsg(
+      'ProcessAnalysisAbout',
+      JSON.stringify({
+        title,
+        data: msg,
+      }),
+    );
+  };
+
+  const onTabChange = (tab) => {
+    setTab(tab);
+    setSelectedRowKeys(null);
+    UnityAction.sendMsg('ProcessAnalysisType', tab);
+  };
+
+  // const rowSelection = {
+  //   type: 'radio',
+  //   selectedRowKeys,
+  //   onChange: (selectedRowKeys, selectedRows) => {
+  //     setSelectedRowKeys(selectedRowKeys);
+  //     UnityAction.sendMsg('SynDev', selectedRows[0].DeviceCode);
+  //   },
+  // };
+
+  const onSelectRow = (record, index) => {
+    // const selectedList = [...selectedRowKeys];
+    // if (selectedList[0] === index) return;
+    // selectedList[0] = index;
+    setSelectedRowKeys(index);
+    UnityAction.sendMsg('SynDev', record.DeviceCode);
+      };
+
+  const setRowClassName = (record, index) => {
+    if (!selectedRowKeys && selectedRowKeys !== 0) return;
+    if (selectedRowKeys == index || selectedRowKeys == record.DeviceCode) {
+      return styles.tableSelect;
+    }
+  };
+
+  return (
+    <Spin spinning={loading}>
+      <TabsContent
+        small={true}
+        center={false}
+        defaultActiveKey="1"
+        items={data?.map((item) => {
+          return {
+            label: `${item.name}(${item.data?.length || 0})`,
+            key: item.type,
+            children: (
+              <Table
+                dataSource={item.data}
+                columns={columns}
+                rowKey={'index'}
+                // rowSelection={rowSelection}
+                rowClassName={setRowClassName}
+                onRow={(record, index) => ({
+                  onClick: () => onSelectRow(record, index),
+                })}
+                pagination={false}
+                scroll={{ y: document.body.clientHeight - 730 }}
+              />
+            ),
+          };
+        })}
+        onChange={onTabChange}
+      />
+    </Spin>
+  );
+};
+export default connect(({ smartOps, loading }) => ({
+  loading: loading.effects['smartOps/queryList'],
+  list: smartOps.list.list,
+  processList: smartOps.processList,
+}))(Analysis);

+ 216 - 0
src/pages/SmartOps/ChartPage.js

@@ -0,0 +1,216 @@
+import ChartModule from '@/components/ManagementPage/chartModule';
+import PageContent from '@/components/PageContent';
+import { getDeviceRealDataByTime } from '@/services/SmartOps';
+import { useParams, useSearchParams } from '@umijs/max';
+import { Button, DatePicker, Empty, Form, Spin } from 'antd';
+import dayjs from 'dayjs';
+import { useEffect, useState } from 'react';
+import styles from './index.less';
+
+const { RangePicker } = DatePicker;
+const tabs = [
+  {
+    label: '近一天',
+    value: 24,
+    timeRange: [dayjs().subtract(1, 'day'), dayjs()],
+  },
+  {
+    label: '近一周',
+    value: 24 * 7,
+    timeRange: [dayjs().subtract(7, 'day'), dayjs()],
+  },
+  {
+    label: '近一个月',
+    value: 24 * 30,
+    timeRange: [dayjs().subtract(1, 'month'), dayjs()],
+  },
+];
+
+const ChartPage = (props) => {
+  const {
+    // location: {
+    //   query: { type = 2, data },
+    // },
+  } = props;
+
+  const { projectId } = useParams();
+  const [searchParams, setSearchParams] = useSearchParams();
+  const type = searchParams.get('type') || 2;
+  const data = searchParams.get('data');
+
+  const [loading, setLoading] = useState(false);
+  const [options, setOptions] = useState({});
+  const [timeActive, setTimeActive] = useState(0);
+  const [timeRange, setTimeRange] = useState([
+    dayjs().subtract(1, 'day'),
+    dayjs(),
+  ]);
+
+  const defaultSearch = {
+    deviceid: '',
+    dataitemid: '',
+    project_id: projectId,
+    stime: dayjs().subtract(1, 'day').valueOf(),
+    etime: dayjs().valueOf(),
+    size: 10,
+    interval: 'minute',
+    aggregator: 'realtime',
+  };
+
+  const changeTime = async (timeType, date) => {
+    const values = { ...defaultSearch };
+    let diffDay;
+    // values.timeType = timeType;
+    if (date) {
+      const [startDate, endDate] = date;
+      values.etime = endDate * 1;
+      values.stime = startDate * 1;
+      diffDay = endDate.diff(startDate, 'days');
+    } else {
+      let currentDate = dayjs();
+      values.etime = currentDate * 1;
+      values.stime = currentDate.subtract(timeType, 'hour') * 1;
+      diffDay = (timeType / 24).toFixed(0);
+    }
+    if (diffDay >= 15 && diffDay < 30) {
+      // 15-30天 时间间隔不能为分钟
+      values.interval = 'h';
+      values.size = 1;
+    } else if (diffDay >= 30) {
+      // 超过30天 时间间隔只能为天
+      values.interval = 'day';
+      values.size = 1;
+    }
+
+    setLoading(true);
+    const list = JSON.parse(data);
+    const paramsList = list?.map((item) => {
+      return { ...values, ...item }; // deviceid: item.deviceid, dataitemid: item.dataitemid
+    });
+
+    getData(paramsList);
+  };
+
+  const handleTabChange = (tab) => {
+    const params = { ...defaultSearch };
+    switch (tab) {
+      case 1: //日
+        break;
+      case 2: //月
+        params.size = 1;
+        params.interval = 'h';
+        params.stime = dayjs().subtract(1, 'month').valueOf();
+        params.etime = dayjs().valueOf();
+        break;
+      case 3: //年
+        params.size = 1;
+        params.interval = 'day';
+        params.stime = dayjs().subtract(1, 'year').valueOf();
+        params.etime = dayjs().valueOf();
+        break;
+    }
+
+    const list = JSON.parse(data);
+    const paramsList = list?.map((item) => {
+      return {
+        ...params,
+        deviceid: item.deviceid,
+        dataitemid: item.dataitemid,
+      };
+    });
+    getData(paramsList);
+  };
+
+  useEffect(() => {
+    handleTabChange(1);
+  }, []);
+
+  const getData = async (list) => {
+    const data = await Promise.all(
+      list?.map((item) => {
+        return getDeviceRealDataByTime(item).then((res) => res.data);
+      }),
+    );
+    setLoading(false);
+    const options = {
+      yName: '',
+      xData: data.length > 0 && data[0]?.map((item) => item.htime_at),
+      dataList: data?.map((item) => {
+        return {
+          type: 0,
+          name: item?.[0]?.name,
+          data: item?.map((item) => item.val * 1),
+        };
+      }),
+    };
+    // console.log(options);
+
+    if (
+      options.dataList?.every((item) => !item.data || item.data.length == 0)
+    ) {
+      setOptions(null);
+    } else {
+      setOptions(options);
+    }
+  };
+
+  return (
+    <PageContent>
+      <div
+        style={{
+          display: 'flex',
+          alignItems: 'center',
+          justifyContent: 'space-between',
+        }}
+      >
+        <div className={styles.form}>
+          <Form.Item style={{ marginBottom: 0 }}>
+            <RangePicker
+              value={timeRange}
+              showTime={false}
+              disabledDate={(current) =>
+                current && current > dayjs().endOf('day')
+              }
+              onChange={(date) => {
+                setTimeRange(date);
+                setTimeActive(null);
+                changeTime(-1, date);
+              }}
+              allowClear={false}
+            />
+          </Form.Item>
+          <div>
+            {tabs.map((item, index) => (
+              <Button
+                key={item.type}
+                type="primary"
+                style={{
+                  marginRight: 20,
+                  background: timeActive == index ? '#329BFE' : '#2F4D83',
+                }}
+                onClick={() => {
+                  setTimeActive(index);
+                  changeTime(item.value);
+                  setTimeRange(item.timeRange);
+                }}
+              >
+                {item.label}
+              </Button>
+            ))}
+          </div>
+        </div>
+      </div>
+
+      <Spin spinning={loading}>
+        <div style={{ height: 'calc(100vh - 100px)' }}>
+          {options ? (
+            <ChartModule chartType="line" {...options} />
+          ) : (
+            <Empty style={{ marginTop: 140 }} />
+          )}
+        </div>
+      </Spin>
+    </PageContent>
+  );
+};
+export default ChartPage;

+ 205 - 0
src/pages/SmartOps/HistoryRecord.js

@@ -0,0 +1,205 @@
+import PageContent from '@/components/PageContent';
+import PageTitle from '@/components/PageTitle';
+import ScrollLoading from '@/components/ScrollLoading';
+import { getHistoryRecord } from '@/services/SmartOps';
+import { GetTokenFromUrl, UnityAction } from '@/utils/utils';
+import { useNavigate, useParams, useRequest } from '@umijs/max';
+import { Button, DatePicker, Select, Table } from 'antd';
+import dayjs from 'dayjs';
+import { useEffect, useState } from 'react';
+import styles from './index.less';
+
+const { RangePicker } = DatePicker;
+const { Option } = Select;
+
+const HistoryRecord = (props) => {
+  const { projectId } = useParams();
+  const navigate = useNavigate();
+
+  const convertObject2FormData = (params) => {
+    const formData = new FormData();
+    Object.entries(params).forEach(([key, value]) => {
+      if (value !== null && value !== undefined && value !== NaN) {
+        formData.append(key, value);
+      }
+    });
+    return formData;
+  };
+
+  const defaultParams = {
+    project_id: Number(projectId),
+    start_time: '',
+    end_time: '',
+    page: 1,
+    page_size: 20,
+  };
+  const [data, setData] = useState([]);
+  const [pagination, setPagination] = useState({});
+  const [queryParams, setQueryParams] = useState(defaultParams);
+
+  const [formData, setFormData] = useState(
+    convertObject2FormData(defaultParams),
+  );
+
+  const { run: getList, loading } = useRequest(
+    (params = formData) => getHistoryRecord(params),
+    {
+      onSuccess: (res) => {
+        if (res.pagenation?.current == 1) {
+          setData(res?.list);
+        } else {
+          setData([...data, ...res?.list]);
+        }
+        setPagination(res.pagenation);
+      },
+    },
+  );
+
+  const columns = [
+    {
+      title: '时间',
+      dataIndex: 'CTime',
+      key: 'CTime',
+      render: (text) => {
+        return dayjs(text).format('YYYY-MM-DD HH:mm') || '--';
+      },
+    },
+    {
+      title: '工况分析',
+      dataIndex: 'Num1',
+      key: 'Num1',
+      render: (text) => {
+        if (text === undefined || text === null) {
+          return 0;
+        }
+        return text;
+      },
+    },
+    {
+      title: '设备分析',
+      dataIndex: 'Num4',
+      key: 'Num4',
+      render: (text) => {
+        if (text === undefined || text === null) {
+          return '--';
+        }
+        return text;
+      },
+    },
+    {
+      title: '工艺分析',
+      dataIndex: 'Num2',
+      key: 'Num2',
+      render: (text) => {
+        if (!text) {
+          return 0;
+        } else {
+          return text.split(',').length;
+        }
+      },
+    },
+    {
+      title: '感知分析',
+      dataIndex: 'Num3',
+      key: 'Num3',
+      render: (text) => {
+        if (text === undefined || text === null) {
+          return 0;
+        }
+        return text;
+      },
+    },
+
+    {
+      title: '操作',
+      render: (record) => (
+        <a
+          onClick={() => {
+            navigate(
+              `/smart-ops/${projectId}?time=${record.CTime}&idList=${
+                record.Num2
+              }&patrolId=${record.PatrolId}&JWT-TOKEN=${GetTokenFromUrl()}`,
+            );
+          }}
+        >
+          详情
+        </a>
+      ),
+    },
+  ];
+
+  const handleParamsChange = (key, value) => {
+    const tempParams = {
+      project_id: Number(projectId),
+      start_time: queryParams.start_time || '',
+      end_time: queryParams.end_time || '',
+      page: queryParams.page || 1,
+      page_size: queryParams.page_size || 20,
+    };
+    switch (key) {
+      case 'date':
+        if (value?.length === 2) {
+          tempParams.start_time = dayjs(value[0]).format('YYYY-MM-DD 00:00:00');
+          tempParams.end_time = dayjs(value[1]).format('YYYY-MM-DD 23:59:59');
+        } else {
+          tempParams.start_time = '';
+          tempParams.end_time = '';
+        }
+        break;
+      case 'page':
+        tempParams.page = value;
+        handleSearch(convertObject2FormData(tempParams));
+        break;
+      default:
+        break;
+    }
+    setQueryParams(tempParams);
+  };
+
+  const handleSearch = (params) => {
+    if (params !== undefined) {
+      getList(params);
+      return;
+    }
+    getList(formData);
+  };
+
+  useEffect(() => {
+    const tempFormData = convertObject2FormData(queryParams);
+    // page变更自动请求接口
+    setFormData(tempFormData);
+  }, [queryParams]);
+
+  useEffect(() => {
+    UnityAction.sendMsg('EnterHistoryPage');
+  }, []);
+
+  return (
+    <PageContent>
+      <PageTitle returnable>历史记录</PageTitle>
+      <div className={styles.searchContent}>
+        日期:
+        <RangePicker
+          className={styles.timePicker}
+          onChange={(value) => handleParamsChange('date', value)}
+        />
+        <Button
+          className={styles.marginLeft}
+          type="primary"
+          onClick={() => handleSearch()}
+        >
+          查询
+        </Button>
+      </div>
+      <ScrollLoading
+        height={182}
+        loading={loading}
+        pagination={pagination}
+        handleLoadData={(current) => handleParamsChange('page', current)}
+      >
+        <Table dataSource={data} columns={columns} pagination={false} />
+      </ScrollLoading>
+    </PageContent>
+  );
+};
+export default HistoryRecord;

+ 235 - 0
src/pages/SmartOps/OperationRecord.js

@@ -0,0 +1,235 @@
+import PageContent from '@/components/PageContent';
+import PageTitle from '@/components/PageTitle';
+import ScrollLoading from '@/components/ScrollLoading';
+import { getVarValues } from '@/services/DeviceInfo';
+import { useNavigate, useParams, useRequest } from '@umijs/max';
+import { Button, DatePicker, Select, Table } from 'antd';
+import dayjs from 'dayjs';
+import { useEffect, useState } from 'react';
+import styles from './index.less';
+
+const { RangePicker } = DatePicker;
+const { Option } = Select;
+
+const OperationRecord = (props) => {
+  const { projectId } = useParams();
+  const navigate = useNavigate();
+
+  const convertObject2FormData = (params) => {
+    const formData = new FormData();
+    Object.entries(params).forEach(([key, value]) => {
+      if (value !== null && value !== undefined && value !== NaN) {
+        formData.append(key, value);
+      }
+    });
+    return formData;
+  };
+
+  const defaultParams = {
+    project_id: Number(projectId),
+    s_time: '',
+    e_time: '',
+    cause_type: '',
+    currentPage: 1,
+    pageSize: 20,
+  };
+  const [data, setData] = useState([]);
+  const [pagination, setPagination] = useState({});
+  const [queryParams, setQueryParams] = useState(defaultParams);
+
+  const [formData, setFormData] = useState(
+    convertObject2FormData(defaultParams),
+  );
+
+  const { run: getList, loading } = useRequest(
+    (params = formData) => getVarValues(params),
+    {
+      onSuccess: (res) => {
+        if (res.pagination?.current == 1) {
+          setData(res?.list);
+        } else {
+          setData([...data, ...res?.list]);
+        }
+        setPagination(res.pagination);
+      },
+    },
+  );
+
+  const columns = [
+    {
+      title: '操作时间',
+      dataIndex: 'c_time',
+      key: 'c_time',
+      align: 'center',
+      render: (value) => {
+        return <div>{dayjs(value).format('YYYY-MM-DD HH:mm')}</div>;
+      },
+    },
+    {
+      title: '来源',
+      dataIndex: 'cause_type',
+      key: 'cause_type',
+      render: (text) => {
+        if (Number(text) === 0) {
+          return '自主操作';
+        }
+        return '工况建议';
+      },
+    },
+    {
+      title: '点位名称',
+      dataIndex: 'item_alias',
+      key: 'item_alias',
+      align: 'center',
+      render: (text) => {
+        if (!text) {
+          return '--';
+        }
+        return text;
+      },
+    },
+    {
+      title: '设备名称',
+      dataIndex: 'device_name',
+      key: 'device_name',
+      align: 'center',
+      render: (text) => {
+        if (!text) {
+          return '--';
+        }
+        return text;
+      },
+    },
+
+    {
+      title: '操作前数值',
+      dataIndex: 'old_value',
+      key: 'old_value',
+      align: 'center',
+      render: (text) => {
+        if (!text) {
+          return '--';
+        }
+        return text;
+      },
+    },
+
+    {
+      title: '操作后数值',
+      dataIndex: 'new_value',
+      key: 'new_value',
+      align: 'center',
+      render: (text) => {
+        if (!text) {
+          return '--';
+        }
+        return text;
+      },
+    },
+    {
+      title: '操作人',
+      dataIndex: 'operator_name',
+      key: 'operator_name',
+      align: 'center',
+      render: (text) => {
+        if (!text) {
+          return '--';
+        }
+        return text;
+      },
+    },
+  ];
+
+  const handleParamsChange = (key, value) => {
+    const tempParams = {
+      project_id: Number(projectId),
+      s_time: queryParams.s_time || '',
+      e_time: queryParams.e_time || '',
+      cause_type: queryParams.cause_type || '',
+      currentPage: 1,
+      pageSize: queryParams.pageSize || 20,
+    };
+    switch (key) {
+      case 'cause_type':
+        tempParams[key] = value;
+        break;
+      case 'date':
+        tempParams.currentPage = 1;
+        if (value?.length === 2) {
+          tempParams.s_time = dayjs(value[0]).format('YYYY-MM-DD 00:00:00');
+          tempParams.e_time = dayjs(value[1]).format('YYYY-MM-DD 23:59:59');
+        } else {
+          tempParams.s_time = '';
+          tempParams.e_time = '';
+        }
+        break;
+      case 'page':
+        tempParams.currentPage = value;
+        handleSearch(convertObject2FormData(tempParams));
+      default:
+        break;
+    }
+    setQueryParams(tempParams);
+  };
+
+  const handleSearch = (params) => {
+    if (params !== undefined) {
+      getList(params);
+      return;
+    }
+    getList(formData);
+  };
+
+  useEffect(() => {
+    const tempFormData = convertObject2FormData(queryParams);
+    // page变更自动请求接口
+    setFormData(tempFormData);
+  }, [queryParams]);
+
+  return (
+    <PageContent>
+      <PageTitle returnable>操作记录</PageTitle>
+
+      <div className={styles.searchContent}>
+        {/* <Button
+            className={styles.marginRight}
+            type="primary"
+            onClick={() => navigate(-1)}
+          >
+            返回
+          </Button> */}
+        日期:
+        <RangePicker
+          className={[styles.timePicker, styles.marginRight].join(' ')}
+          onChange={(value) => handleParamsChange('date', value)}
+        />
+        <span style={{ marginLeft: '20px' }}>来源:</span>
+        <Select
+          placeholder="请选择来源"
+          style={{ width: 250 }}
+          onChange={(value) => handleParamsChange('cause_type', value)}
+          allowClear
+        >
+          <Option value="0">自主操作</Option>
+          <Option value="1">工况建议</Option>
+        </Select>
+        <Button
+          className={styles.marginLeft}
+          type="primary"
+          onClick={() => handleSearch()}
+        >
+          查询
+        </Button>
+      </div>
+      <ScrollLoading
+        height={182}
+        loading={loading}
+        pagination={pagination}
+        handleLoadData={(current) => handleParamsChange('page', current)}
+      >
+        <Table dataSource={data || []} columns={columns} pagination={false} />
+      </ScrollLoading>
+    </PageContent>
+  );
+};
+export default OperationRecord;

+ 839 - 0
src/pages/SmartOps/WorkAnalysisDetail.js

@@ -0,0 +1,839 @@
+import PageContent from '@/components/PageContent';
+import PageTitle from '@/components/PageTitle';
+import TabsContent from '@/components/TabsContent';
+import {
+  queryBackwash,
+  queryBackwashList,
+  queryDesignNob,
+  queryDesignNobList,
+  queryDesignWash,
+  queryDesignWashList,
+  queryDrug,
+  queryDrugList,
+  queryMembrane,
+  queryMembraneList,
+  queryPump,
+  queryPumpList,
+} from '@/services/SmartOps';
+import { UnityAction } from '@/utils/utils';
+import {
+  useNavigate,
+  useParams,
+  useRequest,
+  useSearchParams,
+} from '@umijs/max';
+import { Collapse, DatePicker, Empty, Form, Spin } from 'antd';
+import dayjs from 'dayjs';
+import * as echarts from 'echarts';
+import { useEffect, useMemo, useRef, useState } from 'react';
+import styles from './WorkAnalysisDetail.less';
+
+const TYPE = {
+  td_uf: {
+    name: '超滤膜组',
+    device: (params) => queryMembraneList({ ...params, type: 'uf' }),
+    chart: (params) => queryMembrane({ ...params, type: 'uf' }),
+  },
+  td_mf: {
+    name: '微滤膜',
+    device: (params) => queryMembraneList({ ...params, type: 'mf' }),
+    chart: (params) => queryMembrane({ ...params, type: 'mf' }),
+  },
+  td_nf: {
+    name: '纳滤膜',
+    device: (params) => queryMembraneList({ ...params, type: 'nf' }),
+    chart: (params) => queryMembrane({ ...params, type: 'nf' }),
+  },
+  td_ro: {
+    name: '反渗透膜',
+    device: (params) => queryMembraneList({ ...params, type: 'ro' }),
+    chart: (params) => queryMembrane({ ...params, type: 'ro' }),
+  },
+  tdr_pac: {
+    name: '絮凝剂投加',
+    device: (params) => queryDrugList({ ...params, type: 'pac' }),
+    chart: (params) => queryDrug({ ...params, type: 'pac' }),
+  },
+  tdr_hci: {
+    name: 'HCI投加',
+    device: (params) => queryDrugList({ ...params, type: 'hci' }),
+    chart: (params) => queryDrug({ ...params, type: 'hci' }),
+  },
+  tdr_nob: {
+    name: '非氧化杀菌剂投加',
+    device: (params) => queryDrugList({ ...params, type: 'nob' }),
+    chart: (params) => queryDrug({ ...params, type: 'nob' }),
+  },
+  tt_backwash: {
+    name: '反冲洗记录',
+    device: queryBackwashList,
+    chart: queryBackwash,
+  },
+  tt_wash: {
+    name: '大水量冲洗记录',
+    device: queryDesignWashList,
+    chart: queryDesignWash,
+  },
+  tt_nob: {
+    name: '非氧化杀菌记录',
+    device: queryDesignNobList,
+    chart: queryDesignNob,
+  },
+  td_pump_nf: {
+    name: '纳滤水泵',
+    device: (params) => queryPumpList({ ...params, stage: 'td_pump_nf' }),
+    chart: (params) => queryPump({ ...params, stage: 'td_pump_nf' }),
+  },
+  td_pump_uf: {
+    name: '超滤水泵',
+    device: (params) => queryPumpList({ ...params, stage: 'td_pump_uf' }),
+    chart: (params) => queryPump({ ...params, stage: 'td_pump_uf' }),
+  },
+  td_pump_ro: {
+    // td_pump: {
+    name: '反渗透水泵',
+    device: (params) => queryPumpList({ ...params, stage: 'td_pump_ro' }),
+    chart: (params) => queryPump({ ...params, stage: 'td_pump_ro' }),
+  },
+  td_pump_mf: {
+    name: '微滤水泵',
+    device: (params) => queryPumpList({ ...params, stage: 'td_pump_mf' }),
+    chart: (params) => queryPump({ ...params, stage: 'td_pump_mf' }),
+  },
+  td_pump_nf_drug: {
+    name: '加药泵',
+    device: (params) => queryPumpList({ ...params, stage: 'td_pump_nf_drug' }),
+    chart: (params) => queryPump({ ...params, stage: 'td_pump_nf_drug' }),
+  },
+  td_pump_uf_drug: {
+    name: '加药泵',
+    device: (params) => queryPumpList({ ...params, stage: 'td_pump_uf_drug' }),
+    chart: (params) => queryPump({ ...params, stage: 'td_pump_uf_drug' }),
+  },
+  td_pump_ro_drug: {
+    // td_pump: {
+    name: '加药泵',
+    device: (params) => queryPumpList({ ...params, stage: 'td_pump_ro_drug' }),
+    chart: (params) => queryPump({ ...params, stage: 'td_pump_ro_drug' }),
+  },
+  td_pump_mf_drug: {
+    name: '加药泵',
+    device: (params) => queryPumpList({ ...params, stage: 'td_pump_mf_drug' }),
+    chart: (params) => queryPump({ ...params, stage: 'td_pump_mf_drug' }),
+  },
+};
+
+const { Panel } = Collapse;
+const { RangePicker } = DatePicker;
+const DateTab = [
+  {
+    value: 'day',
+    label: '今日',
+  },
+  {
+    value: 'week',
+    label: '本周',
+  },
+  {
+    value: 'month',
+    label: '本月',
+  },
+];
+
+function WorkAnalysisDetail(props) {
+  const navigate = useNavigate();
+  const { projectId } = useParams();
+  const [searchParams, setSearchParams] = useSearchParams();
+  const eTime = searchParams.get('eTime');
+
+  const workAnalysis = useMemo(() => {
+    let workAnalysis = {
+      technologys: [],
+      optimizationItems: {},
+      optimizationNumber: 0,
+    };
+    try {
+      workAnalysis = JSON.parse(sessionStorage.workAnalysis);
+      workAnalysis.technologys = workAnalysis.technologys.filter((item) => {
+        if (!TYPE[item]) console.log(item);
+        return TYPE[item];
+      });
+    } catch (error) {
+      console.error(error);
+    }
+    return workAnalysis;
+  }, []);
+  const {
+    technologys,
+    optimizationItems,
+    optimizationNumber,
+    parentName,
+    name,
+  } = workAnalysis;
+
+  const [active, setActive] = useState(technologys[0]);
+  const [current, setCurrent] = useState([]);
+  const [selected, setSelected] = useState('');
+
+  const handleBackClick = () => {
+    UnityAction.sendMsg('ProcessAnalysisDetailBack');
+    navigate(-1);
+  };
+
+  useEffect(() => {
+    UnityAction.addEventListener('SynDev', (deviceCode) =>
+      setCurrent([deviceCode]),
+    );
+    return () => UnityAction.off('SynDev');
+  }, []);
+
+  return (
+    <PageContent closeable={false}>
+      <PageTitle onReturn={handleBackClick}>返回</PageTitle>
+      <div className={styles.pageContent}>
+        <div className={styles.title}>
+          {parentName}:{name}
+          <span>
+            {optimizationNumber > 0 && `${optimizationNumber}项待优化`}
+          </span>
+        </div>
+        <TabsContent
+          center={false}
+          small
+          defaultActiveKey={active}
+          items={technologys.map((item) => ({
+            label: TYPE[item]?.name,
+            key: item,
+            children: (
+              <ListContent
+                tab={active}
+                current={active == item ? current : []}
+                setCurrent={setCurrent}
+                selected={selected}
+                setSelected={setSelected}
+                name={name}
+                active={item}
+                optimizationItems={optimizationItems}
+                projectId={projectId}
+                eTime={eTime}
+              />
+            ),
+          }))}
+          onChange={(key) => {
+            setCurrent([]);
+            setActive(key);
+          }}
+        ></TabsContent>
+      </div>
+    </PageContent>
+  );
+}
+const ListContent = (props) => {
+  const {
+    tab,
+    projectId,
+    active,
+    optimizationItems = {},
+    name,
+    current,
+    setCurrent,
+    eTime,
+    selected,
+    setSelected,
+  } = props;
+
+  const { data, loading, run } = useRequest(
+    () => {
+      let params = {
+        page: 1,
+        page_size: 99999,
+        project_id: projectId,
+      };
+      return TYPE[active]?.device(params);
+    },
+    {
+      formatResult: (res) => {
+        return res.data.list;
+      },
+    },
+  );
+
+  useEffect(() => {
+    if (tab == active && data) {
+      sendMsg();
+    }
+    UnityAction.on('SynDev', selectedItem);
+    return () => UnityAction.off('SynDev');
+  }, [tab, active, data]);
+
+  const selectedItem = (e) => {
+    setSelected(e);
+    setCurrent(current => [...current, `${active}&&${e}`]);
+  };
+
+  //通知unity切换tab发送tab数据
+  const sendMsg = () => {
+    const SysDevs = {};
+    data?.forEach((item) => {
+      SysDevs[item.device_code] =
+        optimizationItems?.[`${active}:${item.device_code}`] || 0;
+    });
+    const msg = { SysName: name, SysDevs };
+    UnityAction.sendMsg('ProcessAnalysisDetail', JSON.stringify(msg));
+  };
+
+  // const activeKeys = useMemo(() => {
+  //   if (!data) return [];
+  //   return current.filter((deviceCode) =>
+  //     data.some((item) => item.device_code == deviceCode),
+  //   );
+  // }, [data, current]);
+
+  const handleClick = (keys) => {
+    // 判断是开启还是关闭
+    // 设置当前选中行
+    if (keys.length > current.length) {
+      const code = keys[keys.length - 1];
+      UnityAction.sendMsg('SynDev', code.split('&&')[1]);
+      setSelected(code);
+    } else if (current.length > 0) {
+      const item = current.find(
+        (item) => keys.findIndex((cur) => cur == item) == -1,
+      );
+      setSelected(item);
+    }
+    setCurrent(keys);
+  };
+
+  const getExtra = (deviceCode) => {
+    if (optimizationItems?.[`${active}:${deviceCode}`]) {
+      return <span style={{ color: '#F5A623' }}>可优化</span>;
+    } else {
+      return <span style={{ color: '#11EEE4' }}>暂无优化</span>;
+    }
+  };
+
+  const getStyle = (code) => {
+    return code == selected ? { backgroundColor: '#1b73c5' } : null;
+  };
+
+  return (
+    <Spin spinning={loading} style={{ minHeight: 300 }}>
+      <Collapse
+        activeKey={current}
+        expandIconPosition="right"
+        onChange={handleClick}
+      >
+        {data?.map((item) => (
+          <Panel
+            style={getStyle(item.device_code)}
+            header={`${item.device_name}(${item.device_code})`}
+            key={`${active}&&${item.device_code}`}
+            extra={getExtra(item.device_code)}
+          >
+            <ChartContent
+              active={active}
+              deviceCode={item.device_code}
+              projectId={projectId}
+              eTime={eTime}
+            />
+          </Panel>
+        ))}
+      </Collapse>
+    </Spin>
+  );
+};
+
+const ChartContent = (props) => {
+  const { deviceCode, projectId, active, eTime } = props;
+  const [visible, setVisible] = useState(false);
+  const [params, setParams] = useState(null);
+  const [time, setTime] = useState([
+    dayjs(eTime).subtract(10, 'minute'),
+    dayjs(eTime),
+  ]);
+  const [dateActive, setDateActive] = useState();
+  const timerRef = useRef({
+    s_time: time[0].format('YYYY-MM-DD HH:mm:ss'),
+    e_time: time[1].format('YYYY-MM-DD HH:mm:ss'),
+  });
+  const domRef = useRef(null);
+  const chartRef = useRef(null);
+
+  const { data, loading, run } = useRequest(() => {
+    let params = {
+      device_code: deviceCode,
+      page: 1,
+      page_size: 9999,
+      project_id: projectId,
+      ...timerRef.current,
+    };
+    return TYPE[active].chart(params);
+  });
+
+  const optimization = useMemo(() => {
+    const result = data?.list?.filter((item) => item.optimization);
+    if (result?.length > 0) {
+      const lastItem = result[result.length - 1];
+      const arr = [
+        'ro_wash_interval',
+        'peb_interval',
+        'pac_fr',
+        'ro_nob_interva',
+      ];
+      const valueList = Object.entries(lastItem.optimization)
+        .map(([key, item]) => {
+          if (!arr.includes(key)) return null;
+          return item.remark;
+        })
+        .filter((item) => item);
+      if (valueList.length == 0) return '';
+      return `【${lastItem.c_time}】${valueList.join(',')}`;
+    }
+    return '';
+  }, [data]);
+
+  const searchTime = (type) => {
+    setDateActive(type);
+    let time = [dayjs().startOf(type), dayjs()];
+    onSearch?.(time);
+  };
+
+  const onSearch = (time) => {
+    console.log(time);
+    if (time && time.length == 2) {
+      let s_time, e_time;
+      s_time = time[0].format('YYYY-MM-DD HH:mm:ss');
+      e_time = time[1].format('YYYY-MM-DD HH:mm:ss');
+
+      timerRef.current = { s_time, e_time };
+      // setTime(time);
+      run();
+    }
+  };
+
+  useEffect(() => {
+    if (data && data.list && domRef.current) {
+      chartRef.current = echarts.init(domRef.current);
+      chartRef.current.clear();
+      let options = getOption(data.list, active);
+      chartRef.current.setOption(options, true);
+      chartRef.current.resize();
+    }
+    return () => {
+      if (chartRef.current) {
+        chartRef.current.dispose();
+      }
+    };
+  }, [data, domRef.current]);
+
+  return (
+    <div className={styles.chartBox}>
+      <Form layout="inline" className={styles.form}>
+        <RangePicker
+          style={{ width: 250 }}
+          allowClear={false}
+          defaultValue={time}
+          // value={time}
+          onChange={onSearch}
+          format="YYYY-MM-DD HH:mm:ss"
+        ></RangePicker>
+        <div className={styles.dateTabs}>
+          {DateTab.map((item) => (
+            <div
+              key={item.value}
+              className={`${styles.dateTabsItem} ${
+                item.value == dateActive ? styles.active : ''
+              }`}
+              onClick={() => searchTime(item.value)}
+            >
+              {item.label}
+            </div>
+          ))}
+        </div>
+      </Form>
+      <div>
+        <Spin spinning={loading}>{!data?.list && <Empty />}</Spin>
+        {data?.list && (
+          <div
+            ref={domRef}
+            style={{
+              height: 300,
+              width: '100%',
+            }}
+            onResize={() => console.log(1)}
+          />
+        )}
+      </div>
+      {optimization && (
+        <div
+          style={{
+            fontSize: 22,
+            color: '#fff',
+            textIndent: 30,
+            padding: '10px 0',
+            flexShrink: 1,
+          }}
+        >
+          {optimization}
+        </div>
+      )}
+
+      {/* <MembraneModal visible={visible} onCancel={() => setVisible(false)} params={params} /> */}
+    </div>
+  );
+};
+function getOption(data = [], active) {
+  let formatter,
+    yAxisName = '',
+    y2AxisName = '',
+    series = [],
+    xAxis = [];
+  var data1 = [],
+    data2 = [],
+    data3 = [],
+    data4 = [];
+  switch (active) {
+    case 'tt_backwash':
+      yAxisName = '清洗周期/Min';
+      formatter = (params) => {
+        let item = data[params[0].dataIndex];
+        let content = '';
+        if (item.bw_type == 1) {
+          // PEB
+          content += 'PEB 反洗开始时间:' + item.peb_st;
+          content += '<br />PEB 反洗结束时间:' + item.peb_et;
+        } else {
+          // CEB
+          content += 'CEB 反洗开始时间:' + item.ceb_st;
+          content += '<br />CEB 反洗结束时间:' + item.ceb_et;
+          content += '<br />CEB 清洗剂浓度' + item.ceb_ppm;
+        }
+        return content;
+      };
+      data?.forEach((item) => {
+        let time = 0;
+        if (item.bw_type == 1) {
+          if (item.peb_et && item.peb_st) {
+            time = dayjs(item.peb_st).diff(dayjs(item.peb_et), 'minutes');
+          }
+          // 实际冲洗
+          data1.push(time);
+          data2.push(null);
+          xAxis.push(dayjs(item.peb_st).format('YYYY-MM-DD HH:mm:ss'));
+        } else {
+          if (item.ceb_st && item.ceb_et) {
+            time = dayjs(item.ceb_st).diff(dayjs(item.ceb_et), 'minutes');
+          }
+          data1.push(null);
+          data2.push(time);
+          xAxis.push(dayjs(item.ceb_st).format('YYYY-MM-DD HH:mm:ss'));
+        }
+      });
+      series = [
+        {
+          name: '物理实际冲洗',
+          type: 'bar',
+          barMaxWidth: '20px',
+          data: data1,
+        },
+        {
+          name: '物理模拟冲洗',
+          type: 'bar',
+          barMaxWidth: '20px',
+          data: data2,
+        },
+        {
+          name: '化学实际冲洗',
+          type: 'bar',
+          barMaxWidth: '20px',
+          data: data3,
+        },
+        {
+          name: '化学模拟冲洗',
+          type: 'bar',
+          barMaxWidth: '20px',
+          data: data4,
+        },
+      ];
+      break;
+    case 'tt_wash':
+      yAxisName = '清洗周期/Min';
+      formatter = (params) => {
+        let item = data[params[0].dataIndex];
+        let content = '';
+        content += '反洗开始时间:' + item.st;
+        content += '<br />反洗结束时间:' + item.et;
+        return content;
+      };
+      data?.forEach((item) => {
+        // 实际冲洗
+        data1.push(Math.ceil(item.interval / 60));
+        // TODO:模拟冲洗
+        data2.push(Math.ceil(item.interval / 60));
+        xAxis.push(dayjs(item.st).format('YYYY-MM-DD HH:mm:ss'));
+      });
+      series = [
+        {
+          name: '实际冲洗',
+          type: 'bar',
+          barMaxWidth: '20px',
+          data: data1,
+        },
+        {
+          name: '模拟冲洗',
+          type: 'bar',
+          barMaxWidth: '20px',
+          data: data2,
+        },
+      ];
+      break;
+    case 'tt_nob':
+      yAxisName = '杀菌周期/Min';
+      formatter = (params) => {
+        let item = data[params[0].dataIndex];
+        let content = '';
+        content += '杀菌开始时间:' + (item.st || '-');
+        content += '<br />杀菌结束时间:' + (item.et || '-');
+        return content;
+      };
+      var data1 = [],
+        data2 = [];
+      data?.forEach((item) => {
+        // 实际冲洗
+        data1.push(Math.ceil(item.interval / 60));
+        // TODO:模拟冲洗
+        data2.push(Math.ceil(item.interval / 60));
+        xAxis.push(dayjs(item.st).format('YYYY-MM-DD HH:mm:ss'));
+      });
+      series = [
+        {
+          name: '实际',
+          type: 'bar',
+          barMaxWidth: '20px',
+          data: data1,
+        },
+        {
+          name: '模拟',
+          type: 'bar',
+          barMaxWidth: '20px',
+          data: data2,
+        },
+      ];
+      break;
+    case 'tdr_hci':
+    case 'tdr_nob':
+    case 'tdr_pac':
+      yAxisName = '投加量';
+      data?.forEach((item) => {
+        // 实际冲洗
+        data1.push(Math.ceil(item.fr / 60));
+        // TODO:模拟冲洗
+        data2.push(Math.ceil(item.fcoa / 60));
+        xAxis.push(dayjs(item.c_time).format('YYYY-MM-DD HH:mm:ss'));
+      });
+      series = [
+        {
+          name: '实际物理投加量',
+          type: 'bar',
+          barMaxWidth: '20px',
+          data: data1,
+        },
+        {
+          name: '理论物理投加量',
+          type: 'bar',
+          barMaxWidth: '20px',
+          data: data2,
+        },
+      ];
+      break;
+
+    case 'td_uf':
+    case 'td_mf':
+    case 'td_nf':
+      yAxisName = '渗透率';
+      data?.forEach((item) => {
+        // 实际跨膜压差
+        data1.push(item.std_tmp);
+        // 模拟跨膜压差
+        data2.push(item.std_permeability);
+        xAxis.push(dayjs(item.c_time).format('YYYY-MM-DD HH:mm:ss'));
+      });
+      series = [
+        {
+          name: '标准跨膜压差',
+          type: 'line',
+          data: data1,
+          showSymbol: false,
+        },
+        {
+          name: '标准渗透率',
+          type: 'line',
+          data: data2,
+          showSymbol: false,
+        },
+      ];
+      break;
+    case 'td_ro':
+      yAxisName = '跨膜压差';
+      data?.forEach((item) => {
+        // 实际跨膜压差
+        data1.push(item.extend['1st_Stage_DP']);
+        data2.push(item.extend['2nd_Stage_DP']);
+        // 模拟跨膜压差
+        data3.push(item.stabilize_extend['1st_Stage_DP']);
+        data4.push(item.stabilize_extend['2nd_Stage_DP']);
+        xAxis.push(dayjs(item.c_time).format('YYYY-MM-DD HH:mm:ss'));
+      });
+      series = [
+        {
+          name: '实际一段跨膜压差',
+          type: 'line',
+          data: data1,
+          showSymbol: false,
+        },
+        {
+          name: '实际二段跨膜压差',
+          type: 'line',
+          data: data2,
+          showSymbol: false,
+        },
+        {
+          name: '模拟一段跨膜压差',
+          type: 'line',
+          data: data3,
+          showSymbol: false,
+        },
+        {
+          name: '模拟二段跨膜压差',
+          type: 'line',
+          data: data4,
+          showSymbol: false,
+        },
+      ];
+      break;
+    case 'td_pump_nf':
+    case 'td_pump_uf':
+    case 'td_pump_ro':
+    // case 'td_pump':
+    case 'td_pump_mf':
+    case 'td_pump_nf_drug':
+    case 'td_pump_mf_drug':
+    case 'td_pump_mf_drug':
+    case 'td_pump_mf_drug':
+      yAxisName = '频率 Hz';
+      y2AxisName = '电流 A';
+      data?.forEach((item) => {
+        // 实际跨膜压差
+        data1.push(item.frequency);
+        data2.push(item.current);
+        xAxis.push(dayjs(item.c_time).format('YYYY-MM-DD HH:mm:ss'));
+      });
+      series = [
+        {
+          name: '频率',
+          type: 'line',
+          data: data1,
+          showSymbol: false,
+        },
+        {
+          name: '电流',
+          type: 'line',
+          data: data2,
+          yAxisIndex: 1,
+          showSymbol: false,
+        },
+      ];
+      break;
+  }
+  // 过滤失效数据
+  xAxis.forEach((time, index) => {
+    if (time === 'Invalid date') {
+      xAxis[index] = null;
+      data1[index] = null;
+      data2[index] = null;
+      data3[index] = null;
+      data4[index] = null;
+    }
+  });
+  xAxis = xAxis.filter((item) => item);
+  data1 = data1.filter((item) => !isNaN(item));
+  data2 = data2.filter((item) => !isNaN(item));
+  data3 = data3.filter((item) => !isNaN(item));
+  data4 = data4.filter((item) => !isNaN(item));
+
+  const option = {
+    color: ['#FFC800', '#30EDFD', '#4096ff', '#ff4d4f', '#ffa940'],
+    tooltip: {
+      trigger: 'axis',
+      axisPointer: {
+        type: 'shadow',
+      },
+      textStyle: {
+        fontSize: 24,
+      },
+      formatter,
+    },
+    legend: {
+      textStyle: {
+        // color: '#fff',
+        fontSize: 24,
+      },
+      type: 'scroll',
+    },
+    grid: {
+      top: 80,
+      left: '3%',
+      right: '4%',
+      bottom: '3%',
+      containLabel: true,
+    },
+    xAxis: {
+      type: 'category',
+      data: xAxis,
+      nameTextStyle: {
+        fontSize: 24,
+      },
+      axisLabel: {
+        fontSize: 24,
+      },
+    },
+    yAxis: {
+      name: yAxisName,
+      type: 'value',
+      boundaryGap: [0, 0.01],
+      nameTextStyle: {
+        fontSize: 24,
+      },
+      axisLabel: {
+        fontSize: 24,
+      },
+      splitNumber: 5,
+    },
+    series,
+  };
+  if (y2AxisName) {
+    let y1 = option.yAxis;
+    let y2 = JSON.parse(JSON.stringify(y1));
+    y1.max = getMax(data1) || 1;
+    y1.interval = y1.max / 5;
+
+    y2.name = y2AxisName;
+    y2.max = getMax(data2) || 1;
+    y2.interval = y2.max / 5;
+    option.yAxis = [y1, y2];
+  }
+  console.log(option);
+  return option;
+}
+
+function getMax(arr) {
+  const max = Math.max(...arr);
+  const exponent = Math.floor(Math.log10(max));
+  const base = Math.pow(10, exponent);
+  const remainder = max % base;
+  const maxRoundUp = max - remainder + base;
+  const maxFixed = maxRoundUp.toFixed(1); // 将最大值的小数位数限制为 1
+  return Number(maxFixed); // 将字符串转换为数字并返回
+}
+export default WorkAnalysisDetail;

+ 92 - 0
src/pages/SmartOps/WorkAnalysisDetail.less

@@ -0,0 +1,92 @@
+.title {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  font-size: 32px;
+  color: #4a90e2;
+  padding: 40px 20px;
+  margin-top: 52px;
+
+  background: #d9e7f9;
+  box-shadow: 0px 0px 12px 7px rgba(40, 94, 120, 0.03);
+  border-radius: 16px;
+  span {
+    color: #f5a623;
+  }
+}
+
+.form {
+  margin-bottom: 14px;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+}
+.dateTabs {
+  display: flex;
+  border: 1px solid #d5d5d5;
+  height: 60px;
+  .dateTabsItem {
+    width: 128px;
+    text-align: center;
+    cursor: pointer;
+    border-right: 1px solid #d5d5d5;
+    line-height: 60px;
+    font-size: 24px;
+    font-weight: 400;
+    color: #4a4a4a;
+    &:last-child {
+      border-right: none;
+    }
+    &.active {
+      background: #4a90e2;
+      color: #fff;
+    }
+  }
+}
+.chartBox {
+  padding-bottom: 40px;
+}
+.pageContent {
+  :global {
+    .ant-collapse {
+      border: none !important;
+    }
+    .ant-collapse > .ant-collapse-item {
+      border-radius: 8px;
+      box-shadow: 0px 0px 8px 2px rgba(191, 191, 191, 0.2);
+      border: 1px solid #eee;
+      background: #4a4a4a;
+      padding: 0 30px;
+      margin-bottom: 30px;
+      > .ant-collapse-header {
+        background: transparent;
+        color: #4a4a4a;
+        height: 108px;
+        font-size: 28px;
+        padding: 0;
+        border-bottom: 1px solid transparent;
+        display: flex;
+        align-items: center;
+        .ant-collapse-arrow {
+          font-size: 28px;
+          right: 0;
+          color: #c0c2c8;
+        }
+        .ant-collapse-extra {
+          position: absolute;
+          right: 30px;
+        }
+      }
+    }
+    .ant-collapse-expand-icon {
+      order: 0 !important;
+    }
+    .ant-collapse-content {
+      padding: 0;
+      border-top: none;
+    }
+    .ant-collapse-content-box {
+      padding: 0 !important;
+    }
+  }
+}

+ 61 - 0
src/pages/SmartOps/components/BarModal.js

@@ -0,0 +1,61 @@
+import DateRadio from '@/components/DateRadio';
+import BarChartModule from '@/components/ManagementPage/BarChartModule';
+import ChartModule from '@/components/ManagementPage/chartModule';
+import { Modal } from 'antd';
+import { useState } from 'react';
+
+const BarModal = ({ type = 1, title, data, visible, onCancel, onChange }) => {
+  const [tab, setTab] = useState('1');
+
+  const option = {
+    yName: '水量(t)',
+    xData: ['00:00', '01:15', '02:30', '03:45', '05:00', '06:15', '07:30'],
+    dataList: [
+      {
+        type: 0,
+        name: '进水水量',
+        data: [820, 932, 901, 934, 1290, 1330, 1320],
+      },
+      {
+        type: 0,
+        name: '进水水量111',
+        data: [420, 932, 601, 934, 1990, 1330, 1520],
+      },
+      {
+        type: 0,
+        name: '预测出水量',
+        data: [720, 832, 601, 834, 1190, 1230, 1220],
+      },
+      {
+        type: 0,
+        name: '实际出水量',
+        data: [820, 632, 501, 534, 1190, 1130, 1120],
+      },
+    ],
+  };
+
+  return (
+    <Modal
+      title={title}
+      visible={visible}
+      onCancel={onCancel}
+      footer={null}
+      width={600}
+    >
+      <DateRadio onChange={(tab) => onChange(tab)} />
+
+      <div style={{ height: '600px' }}>
+        {type == 1 && (
+          <BarChartModule
+            xData={['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']}
+            dataList={[
+              { name: 'xzhou ', data: [120, 200, 150, 80, 70, 110, 130] },
+            ]}
+          />
+        )}
+        {type == 2 && <ChartModule {...option} />}
+      </div>
+    </Modal>
+  );
+};
+export default BarModal;

+ 28 - 0
src/pages/SmartOps/components/BarModal.less

@@ -0,0 +1,28 @@
+.top {
+  margin-bottom: 10px;
+  display: flex;
+  justify-content: right;
+  .tabContent {
+    width: 180px;
+    display: flex;
+    background: #061134;
+    color: #4a90e2;
+    border: 1px solid #4a90e2;
+    .tab {
+      flex-wrap: 0;
+      text-align: center;
+      line-height: 50px;
+      width: 60px;
+      height: 50px;
+      border-right: 1px solid #4a90e2;
+    }
+    .tabActive {
+      .tab;
+      background: #4a90e2;
+      color: #fff;
+    }
+    .tab:last-child {
+      border-right: none;
+    }
+  }
+}

+ 179 - 0
src/pages/SmartOps/components/DeviceAnalysis.js

@@ -0,0 +1,179 @@
+import TabsContent from '@/components/TabsContent';
+import ThresholdDetail from '@/components/ThresholdDetail';
+import { UnityAction } from '@/utils/utils';
+import { connect } from '@umijs/max';
+import { Spin, Table, Tabs } from 'antd';
+import { useEffect, useMemo, useState } from 'react';
+import styles from './DeviceAnalysis.less';
+const { TabPane } = Tabs;
+
+const DeviceAnalysis = (props) => {
+  const Status = [
+    { name: '异常', type: '0', data: [] },
+    { name: '全部', type: '1 ', data: [] },
+  ];
+
+  const { loading, autoReport } = props;
+  const [selectedRowKeys, setSelectedRowKeys] = useState();
+  const [tab, setTab] = useState('1');
+
+  const columns = [
+    {
+      title: '设备名称',
+      render: (record) => (
+        <div>
+          {record.DeviceName}({record.DeviceCode})
+        </div>
+      ),
+    },
+    {
+      title: '巡检项',
+      width: '14%',
+      dataIndex: 'TemplateItem',
+      render: (TemplateItem) => <div>{TemplateItem.Name}</div>,
+    },
+    {
+      title: '设定值范围',
+      width: '40%',
+      render: (record) => (
+        <ThresholdDetail
+          current={record.Value || 0}
+          data={record || {}}
+          // onClick={() => onClickThreshold(record)}
+        />
+      ),
+    },
+    {
+      title: '状态',
+      width: '14%',
+      dataIndex: 'Status',
+      render: (Status) => {
+        switch (Status) {
+          case -1:
+          case 0:
+            return (
+              <div>
+                <i
+                  className={styles.iconStatus}
+                  style={{ background: '#12CEB3' }}
+                ></i>
+                正常
+              </div>
+            );
+          case 1:
+            return (
+              <div>
+                <i
+                  className={styles.iconStatus}
+                  style={{ background: '#FE5850' }}
+                ></i>
+                异常
+              </div>
+            );
+          case 2:
+            return (
+              <div>
+                <i
+                  className={styles.iconStatus}
+                  style={{ background: '#FFE26D' }}
+                ></i>
+                警告
+              </div>
+            );
+        }
+      },
+    },
+  ];
+
+  useEffect(() => {
+    UnityAction.on('SynDev', selectedItem);
+    return () => UnityAction.off('SynDev', selectedItem);
+  }, [data, tab]);
+
+  const selectedItem = (e) => {
+    setSelectedRowKeys(e);
+    console.log(data);
+    const itemIndex = data?.findIndex((item) => item.type == tab);
+    const item = data[itemIndex]?.data?.find((item) => item.DeviceCode == e);
+    if (item) {
+      const dom = document.querySelector(`tr[data-row-key="${item.Id}"]`);
+      if (dom) {
+        let v = document.getElementsByClassName('ant-table-body')[itemIndex];
+        v.scrollTop = dom.offsetTop;
+      }
+    }
+  };
+
+  const data = useMemo(() => {
+    return [
+      { name: '异常', type: '1', data: autoReport?.extendWarningData },
+      { name: '全部', type: '2', data: autoReport?.extendWarningAllData },
+    ];
+  }, [autoReport]);
+
+  const onTabChange = (tab) => {
+    setTab(tab);
+    setSelectedRowKeys(null);
+    UnityAction.sendMsg('DevAnalysisType', tab);
+  };
+
+  // const rowSelection = {
+  //   type: 'radio',
+  //   selectedRowKeys,
+  //   onChange: (selectedRowKeys, selectedRows) => {
+  //     setSelectedRowKeys(selectedRowKeys);
+  //     UnityAction.sendMsg('SynDev', selectedRows[0].DeviceCode);
+  //   },
+  // };
+
+  const onSelectRow = (record, index) => {
+    // const selectedList = [...selectedRowKeys];
+    // if (selectedList[0] === index) return;
+    // selectedList[0] = index;
+    setSelectedRowKeys(index);
+    UnityAction.sendMsg('SynDev', record.DeviceCode);
+  };
+
+  const setRowClassName = (record, index) => {
+    if (!selectedRowKeys && selectedRowKeys !== 0) return;
+    if (selectedRowKeys == index || selectedRowKeys == record.DeviceCode) {
+      return styles.tableSelect;
+    }
+  };
+
+  return (
+    <Spin spinning={loading}>
+      <div className="card-box">
+        <TabsContent
+          small={true}
+          center={false}
+          defaultActiveKey="1"
+          items={data?.map((item) => {
+            return {
+              label: `${item.name}(${item.data?.length || 0})`,
+              key: item.type,
+              children: (
+                <Table
+                  dataSource={item.data}
+                  columns={columns}
+                  rowKey="Id"
+                  rowClassName={setRowClassName}
+                  onRow={(record, index) => ({
+                    onClick: () => onSelectRow(record, index),
+                  })}
+                  pagination={false}
+                  scroll={{ y: document.body.clientHeight - 730 }}
+                />
+              ),
+            };
+          })}
+          onChange={onTabChange}
+        />
+      </div>
+    </Spin>
+  );
+};
+export default connect(({ eqSelfInspection, loading }) => ({
+  loading: loading.effects['eqSelfInspection/getPatrolDataById'],
+  autoReport: eqSelfInspection.autoReport,
+}))(DeviceAnalysis);

+ 11 - 0
src/pages/SmartOps/components/DeviceAnalysis.less

@@ -0,0 +1,11 @@
+.tableSelect {
+  background: rgba(145, 192, 238, 0.16) !important;
+}
+.iconStatus {
+  width: 20px;
+  height: 20px;
+  vertical-align: middle;
+  margin-right: 10px;
+  border-radius: 50%;
+  display: inline-block;
+}

+ 52 - 0
src/pages/SmartOps/components/ThresholdBar.js

@@ -0,0 +1,52 @@
+import React, { useMemo, useRef } from 'react';
+import { Popover } from 'antd';
+import styles from './ThresholdBar.less';
+
+const ThresholdBar = props => {
+  const { current, thresholds = [] } = props;
+  const boxRef = useRef();
+
+  const { min, max } = useMemo(() => {
+    let myThresholds = [...thresholds];
+    if (thresholds.length == 1) {
+      if (thresholds[0] > current) {
+        return { min: '', max: thresholds[0] };
+      } else {
+        return { min: thresholds[0], max: '' };
+      }
+    }
+    myThresholds.sort((a, b) => a - b);
+    let index = myThresholds.findIndex(item => item > current);
+    return { min: myThresholds[index - 1], max: myThresholds[index] };
+  }, [current, thresholds]);
+
+  const getLeft = () => {
+    if (thresholds.length == 1) {
+      if (thresholds[0] > current) {
+        return 0;
+      } else {
+        return '100%';
+      }
+    } else {
+      let number = (current - min) / (max - min);
+      return number * 100 + '%';
+    }
+  };
+
+  return (
+    <div className={styles.bar}>
+      <div className={styles.box} ref={boxRef}>
+        <div className={`${styles.scale} ${styles.min}`}>{min}</div>
+        <div className={`${styles.scale} ${styles.max}`}>{max}</div>
+        <div className={`${styles.scale} ${styles.top}`} style={{ left: getLeft() }}>
+          {parseFloat(current).toFixed(2)}
+        </div>
+        <div className={styles.current} style={{ left: getLeft() }}>
+          <div className={styles.currentBar}></div>
+        </div>
+      </div>
+    </div>
+  );
+};
+
+export default ThresholdBar;

+ 46 - 0
src/pages/SmartOps/components/ThresholdBar.less

@@ -0,0 +1,46 @@
+.bar {
+  padding: 30px 14px;
+}
+
+.box {
+  width: 100%;
+  height: 6px;
+  position: relative;
+  background-color: #D45C41;
+
+}
+.scale {
+  position: absolute;
+  top: 16px;
+  line-height: 1.2;
+  font-size: 16px;
+  word-break: keep-all;
+  transform: translateX(-50%);
+
+  &.top {
+    top: inherit;
+    bottom: 16px;
+  }
+  &.min {
+    left: 0;
+  }
+  &.max {
+    left: 100%;
+  }
+}
+.current {
+  position: absolute;
+  top: 50%;
+  transform: translate(-50%, -50%);
+  z-index: 10;
+  // height: 120%;
+  width: 9px;
+  height: 22px;
+  background: url("@/assets/current.png") no-repeat center;
+  background-size: 100% 100%;
+  // .currentBar {
+  //   height: 100%;
+  //   width: 4px;
+  //   background-color: #fff;
+  // }
+}

+ 503 - 0
src/pages/SmartOps/components/VideoAnalysis.js

@@ -0,0 +1,503 @@
+import ModuleTitle from '@/components/ManagementPage/moduleTitle';
+import TabsContent from '@/components/TabsContent';
+import ThresholdDetail from '@/components/ThresholdDetail';
+import { UnityAction } from '@/utils/utils';
+import { Collapse, Empty, Spin, Table, Tabs } from 'antd';
+import { useEffect, useMemo, useState } from 'react';
+import styles from './VideoAnalysis.less';
+
+const { TabPane } = Tabs;
+const { Panel } = Collapse;
+const TYPE = {
+  video: 1,
+  fill: 2,
+  water: 3,
+};
+
+function VideoAnalysis(props) {
+  const { videoNum, data, videoData, loading } = props;
+  const [tab, setTab] = useState('1');
+  const allCount = Object.values(videoData).reduce(
+    (total, item) => total + (item?.length || 0),
+    0,
+  );
+
+  const [prevKey, setPrevKey] = useState();
+  const [selectedName, setSelectedName] = useState('');
+
+  const errorData = useMemo(() => {
+    const list1 =
+      videoData.dumu_list?.map((item) => {
+        return { ...item, type_name: '视频报警', type: TYPE.video };
+      }) || [];
+    const list2 =
+      videoData.environment_list
+        ?.filter((item) => item.status)
+        .map((item) => {
+          return { ...item, type_name: '环境检测', type: TYPE.fill };
+        }) || [];
+    const list3 =
+      videoData.fluid_level_list
+        ?.filter((item) => item.status)
+        .map((item) => {
+          return { ...item, type_name: '液位监测', type: TYPE.water };
+        }) || [];
+    return [...list1, ...list2, ...list3];
+  }, [videoData]);
+
+  useEffect(() => {
+    UnityAction.on('SynCam', selectedItem);
+    UnityAction.on('SynDev', selectedItem);
+    return () => {
+      UnityAction.off('SynDev', selectedItem);
+      UnityAction.off('SynCam', selectedItem);
+    };
+  }, [videoData]);
+
+  //滚动到相应位置
+  const selectedItem = (e) => {
+    setSelectedName(e);
+    const dom = document.querySelector(`div[data-name="${e}"]`);
+    if (dom) {
+      let v = document.getElementById('videoContent');
+      v.scrollTop = dom.offsetTop;
+    }
+  };
+
+  const onTabChange = (tab) => {
+    setTab(tab);
+    setSelectedName('');
+    UnityAction.sendMsg('SensorAnalysisType', tab);
+  };
+
+  return (
+    <Spin spinning={loading}>
+      <TabsContent
+        small={true}
+        center={false}
+        defaultActiveKey="1"
+        items={[
+          {
+            label: `异常(${videoNum})`,
+            key: '1',
+            children: (
+              <AnalysisContent
+                errorData={errorData}
+                selectedName={selectedName}
+                setSelectedName={setSelectedName}
+              />
+            ),
+          },
+          {
+            label: `全部(${allCount})`,
+            key: '2',
+            children: (
+              <AllContent
+                videoData={videoData}
+                selectedName={selectedName}
+                setSelectedName={setSelectedName}
+              />
+            ),
+          },
+        ]}
+        onChange={onTabChange}
+      />
+    </Spin>
+  );
+}
+
+function AnalysisContent({ errorData, selectedName, setSelectedName }) {
+  const [prevKey, setPrevKey] = useState();
+  const handleImgClick = (item) => {
+    localStorage.setItem('preview', item.path);
+    UnityAction.sendMsg('SensorPic');
+  };
+
+  const handleCollapse = (item) => {
+    const key = item.type == TYPE.video ? item.device_name : item.device_code;
+    const name = item.type == TYPE.video ? 'SynCam' : 'SynDev';
+    // 清除unity发送过来的选中设备
+    setSelectedName('');
+    if (item.key === prevKey) return;
+    setPrevKey(item.key);
+    UnityAction.sendMsg(name, key);
+  };
+
+  const renderContent = (key, item) => {
+    let content = '';
+    switch (key) {
+      case 1:
+        content = (
+          <>
+            <div className={styles.label}>画面报警:</div>
+            <div className={styles.content}>
+              {item.event_type}
+              <img
+                className={styles.img}
+                src={item.path}
+                onClick={() => handleImgClick(item)}
+              />
+            </div>
+          </>
+        );
+        break;
+      case 2:
+        content = (
+          <>
+            <div className={styles.contentFlex}>
+              <div>参数:{item.patrol_name}</div>
+              <div className={styles.threshold}>
+                <span>阈值范围:</span>
+                <div style={{ width: '180px' }}>
+                  <ThresholdDetail
+                    current={item.value || 0}
+                    data={{
+                      JsonNumThreshold: item?.json_num_threshold,
+                      Type: 2,
+                    }}
+                  />
+                </div>
+              </div>
+              <div>
+                状态:
+                {item?.status <= 0 ? (
+                  <span className={styles.textNormal}>正常</span>
+                ) : item.status == 1 ? (
+                  <span className={styles.textAbnormal}>异常</span>
+                ) : (
+                  <span className={styles.textWarning}>警告</span>
+                )}
+              </div>
+            </div>
+          </>
+        );
+        break;
+      case 3:
+        content = (
+          <>
+            <div className={styles.contentFlex}>
+              <div>原液位:{item.origin_value}</div>
+              <div> 差值:{item.value} </div>
+              <div className={styles.threshold}>
+                <span>阈值范围:</span>
+                <div style={{ width: '180px' }}>
+                  <ThresholdDetail
+                    current={item.value || 0}
+                    data={{
+                      JsonNumThreshold: item?.json_num_threshold,
+                      Type: 2,
+                    }}
+                  />
+                </div>
+              </div>
+              <div>
+                状态:
+                {item?.status <= 0 ? (
+                  <span className={styles.textNormal}>正常</span>
+                ) : item.status == 1 ? (
+                  <span className={styles.textAbnormal}>异常</span>
+                ) : (
+                  <span className={styles.textWarning}>警告</span>
+                )}
+              </div>
+            </div>
+          </>
+        );
+        break;
+    }
+    return content;
+  };
+  const getClassName = (item) => {
+    const device =
+      item.type == TYPE.video ? item.device_name : item.device_code;
+    if (selectedName && selectedName == device) {
+      return styles.tableSelect;
+    } else if (prevKey == item.key) {
+      return styles.tableSelect;
+    }
+    return styles.table;
+  };
+
+  return (
+    <div
+      className={styles.page}
+      style={{ height: 'calc(100vh - 630px)', overflow: 'auto' }}
+    >
+      {errorData?.length > 0 ? (
+        errorData.map((item) => (
+          <div
+            data-name={item.device_name}
+            key={item.key}
+            onClick={() => handleCollapse(item)}
+          >
+            <Collapse
+              activeKey={item.key}
+              className={getClassName(item)}
+              expandIcon={() => (
+                <div className={styles.typeText}>{item.type_name}</div>
+              )}
+            >
+              <Panel header={item.device_name} key={item.key}>
+                <div className={styles.box}>
+                  <div className={styles.item}>
+                    {renderContent(item.type, item)}
+                  </div>
+                </div>
+              </Panel>
+            </Collapse>
+          </div>
+        ))
+      ) : (
+        <Empty />
+      )}
+    </div>
+  );
+}
+
+function AllContent({ videoData = {}, selectedName, setSelectedName }) {
+  const {
+    environment_list = [],
+    fluid_level_list = [],
+    dumu_list = [],
+  } = videoData;
+  // const [selectedRowKeys, setSelectedRowKeys] = useState([]);
+  const [selectKey, setSelectKey] = useState([]);
+  const columns1 = [
+    {
+      title: '设备名称',
+      render: (record) => (
+        <div>
+          {record.device_name}({record.device_code})
+        </div>
+      ),
+    },
+    {
+      title: '参数',
+      dataIndex: 'patrol_name',
+      key: 'patrol_name',
+    },
+    {
+      title: '设定值范围',
+      render: (record) => (
+        <ThresholdDetail
+          current={record.value || 0}
+          data={{
+            JsonNumThreshold: record?.json_num_threshold,
+            Type: record.type || 2,
+          }}
+        />
+      ),
+    },
+    {
+      title: '状态',
+      dataIndex: 'status',
+      render: (status) => {
+        switch (status) {
+          case -1:
+          case 0:
+            return (
+              <div>
+                <i
+                  className={styles.iconStatus}
+                  style={{ background: '#12CEB3' }}
+                ></i>
+                正常
+              </div>
+            );
+          case 1:
+            return (
+              <div>
+                <i
+                  className={styles.iconStatus}
+                  style={{ background: '#FE5850' }}
+                ></i>
+                异常
+              </div>
+            );
+          case 2:
+            return (
+              <div>
+                <i
+                  className={styles.iconStatus}
+                  style={{ background: '#FFE26D' }}
+                ></i>
+                警告
+              </div>
+            );
+        }
+      },
+    },
+  ];
+  const columns2 = [
+    {
+      title: '设备名称',
+      render: (record) => (
+        <div>
+          {record.device_name}({record.device_code})
+        </div>
+      ),
+    },
+    {
+      title: '液位数',
+      dataIndex: 'origin_value',
+      key: 'origin_value',
+    },
+    {
+      title: '差值',
+      dataIndex: 'value',
+      key: 'value',
+    },
+    {
+      title: '设定值范围',
+      render: (record) => (
+        <ThresholdDetail
+          current={record.value || 0}
+          data={{
+            JsonNumThreshold: record?.json_num_threshold,
+            Type: record.type || 2,
+          }}
+          // onClick={() => onClickThreshold(record)}
+        />
+      ),
+    },
+    {
+      title: '状态',
+      dataIndex: 'status',
+      render: (status) => {
+        switch (status) {
+          case -1:
+          case 0:
+            return (
+              <div>
+                <i
+                  className={styles.iconStatus}
+                  style={{ background: '#12CEB3' }}
+                ></i>
+                正常
+              </div>
+            );
+          case 1:
+            return (
+              <div>
+                <i
+                  className={styles.iconStatus}
+                  style={{ background: '#FE5850' }}
+                ></i>
+                异常
+              </div>
+            );
+          case 2:
+            return (
+              <div>
+                <i
+                  className={styles.iconStatus}
+                  style={{ background: '#FFE26D' }}
+                ></i>
+                警告
+              </div>
+            );
+        }
+      },
+    },
+  ];
+  const columns3 = [
+    {
+      title: '名称',
+      render: (record) => (
+        <div>
+          {record.device_name}({record.device_code})
+        </div>
+      ),
+    },
+    {
+      title: '描述',
+      dataIndex: 'event_type',
+      key: 'event_type',
+    },
+    {
+      title: '图片',
+      render: (item) => (
+        <img
+          className={styles.img}
+          src={item.path}
+          onClick={() => handleImgClick(item)}
+        />
+      ),
+    },
+  ];
+
+  const onSelectRow = (item, type) => {
+    const key = type == TYPE.video ? item.device_name : item.device_code;
+    const name = type == TYPE.video ? 'SynCam' : 'SynDev';
+    setSelectedName('');
+    if (selectKey === item.key) return;
+    setSelectKey(item.key);
+    UnityAction.sendMsg(name, key);
+  };
+
+  const handleImgClick = (item) => {
+    localStorage.setItem('preview', item.path);
+    UnityAction.sendMsg('SensorPic');
+  };
+
+  const setRowClassName = (item) => {
+    const device =
+      item.type == TYPE.video ? item.device_name : item.device_code;
+    if (selectedName && selectedName == device) {
+      return styles.tableSelect;
+    } else if (selectKey == item.key) {
+      return styles.tableSelect;
+    }
+    return '';
+  };
+
+  return (
+    <div
+      className={styles.page}
+      style={{ height: 'calc(100vh - 630px)', overflow: 'auto' }}
+    >
+      <div className={`card-box ${styles.box}`}>
+        <ModuleTitle title="环境检测" />
+        <Table
+          dataSource={environment_list}
+          columns={columns1}
+          rowClassName={setRowClassName}
+          onRow={(record, index) => ({
+            onClick: () => onSelectRow(record, TYPE.fill),
+          })}
+          pagination={false}
+          scroll={{ y: 400 }}
+        />
+      </div>
+      <div className={`card-box ${styles.box}`}>
+        <ModuleTitle title="液位检测" />
+        <Table
+          dataSource={fluid_level_list}
+          columns={columns2}
+          rowClassName={setRowClassName}
+          onRow={(record, index) => ({
+            onClick: () => onSelectRow(record, TYPE.water),
+          })}
+          pagination={false}
+          scroll={{ y: 400 }}
+        />
+      </div>
+      <div className={`card-box ${styles.box}`}>
+        <ModuleTitle title="感知检测" />
+        <Table
+          dataSource={dumu_list}
+          columns={columns3}
+          rowClassName={setRowClassName}
+          onRow={(record, index) => ({
+            onClick: () => onSelectRow(record, TYPE.video),
+          })}
+          rowKey="id"
+          pagination={false}
+          scroll={{ y: 400 }}
+        />
+      </div>
+    </div>
+  );
+}
+
+export default VideoAnalysis;

+ 111 - 0
src/pages/SmartOps/components/VideoAnalysis.less

@@ -0,0 +1,111 @@
+.box {
+  display: flex;
+  flex-wrap: wrap;
+  justify-content: space-between;
+  .item {
+    width: 100%;
+    margin-bottom: 15px;
+    display: flex;
+    align-items: flex-start;
+    font-size: 22px;
+    line-height: 28px;
+    color: #454444;
+    .label {
+      width: 114px;
+      flex-shrink: 0;
+    }
+    .content {
+      flex: 1;
+    }
+    .contentFlex {
+      width: 100%;
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+    }
+  }
+}
+.iconStatus {
+  width: 20px;
+  height: 20px;
+  vertical-align: middle;
+  margin-right: 10px;
+  border-radius: 50%;
+  display: inline-block;
+}
+
+.img {
+  width: 120px;
+  height: 90px;
+  display: block;
+  margin-top: 10px;
+}
+.page {
+  .tableSelect {
+    background-color: #b0d5f7 !important;
+  }
+  .table {
+    background-color: #ffffff;
+  }
+  .typeText {
+    color: #4a90e2 !important;
+  }
+  .threshold {
+    display: flex;
+    align-items: center;
+  }
+  .textAbnormal {
+    color: #fe5850;
+  }
+  .textNormal {
+    color: #4a90e2;
+  }
+  .box {
+    padding: 16px;
+    margin-bottom: 20px;
+  }
+
+  :global {
+    .ant-collapse .ant-collapse-content {
+      background-color: transparent !important;
+    }
+    .ant-collapse-item {
+      background-color: transparent !important;
+    }
+    .ant-collapse > .ant-collapse-item {
+      border-radius: 8px;
+      box-shadow: 0px 0px 8px 2px rgba(191, 191, 191, 0.2);
+      border: 1px solid #eee;
+      // background: rgba(255, 255, 255);
+      padding: 0 15px;
+      margin-bottom: 15px;
+      &.ant-collapse-item-active > .ant-collapse-header {
+        border-bottom: 1px solid #7a9dcd;
+      }
+      > .ant-collapse-header {
+        background: none;
+        color: rgb(91, 88, 88) 373;
+        height: 62px;
+        font-size: 22px;
+        padding: 0;
+        align-items: center;
+        border-bottom: 1px solid transparent;
+        .ant-collapse-arrow {
+          font-size: 22px;
+          right: 0;
+        }
+        .ant-collapse-extra {
+          position: absolute;
+          right: 30px;
+        }
+      }
+    }
+    .ant-collapse:last-child > .ant-collapse-item {
+      // margin-bottom: 0;
+    }
+    .ant-collapse-content {
+      padding: 15px 0;
+      border-top: none;
+    }
+  }
+}

+ 50 - 0
src/pages/SmartOps/components/WorkAnalysis.js

@@ -0,0 +1,50 @@
+import { RightOutlined } from '@ant-design/icons';
+import { useNavigate } from '@umijs/max';
+import { Spin } from 'antd';
+import styles from './WorkAnalysis.less';
+
+function WorkAnalysis(props) {
+  const navigate = useNavigate();
+  const { projectId, workAnalysisRequest: data, loading, eTime } = props;
+
+  const project_categorys = data?.project_categorys || [];
+  const toDetail = (item) => {
+    sessionStorage.workAnalysis = JSON.stringify(item);
+    navigate(
+      `/smart-ops/work-analysis-detail/${projectId}?typeList=${item.technologys.join(
+        ',',
+      )}&eTime=${eTime}`,
+    );
+  };
+  return (
+    <Spin spinning={loading}>
+      <div style={{ height: 'calc(100vh - 580px)', overflow: 'auto' }}>
+        {project_categorys?.map((item) => (
+          <div className={`${styles.box} card-box`} key={item.type}>
+            <div className={styles.title}>{item.name}</div>
+            <ul className={styles.list}>
+              {item.childs?.map((cItem) => (
+                <li key={cItem.type}>
+                  <div className={styles.listTitle}>{cItem.name}</div>
+                  <div className={styles.btn} onClick={() => toDetail(cItem)}>
+                    {cItem.optimizationNumber == 0 ? (
+                      <span style={{ color: '#12CEB3' }}>暂无优化</span>
+                    ) : (
+                      <span style={{ color: '#F5A623' }}>
+                        可优化({cItem.optimizationNumber})
+                      </span>
+                    )}
+                    <RightOutlined className={styles.icon} />
+                    {/* <Icon className={styles.icon} type="right" /> */}
+                  </div>
+                </li>
+              ))}
+            </ul>
+          </div>
+        ))}
+      </div>
+    </Spin>
+  );
+}
+
+export default WorkAnalysis;

+ 43 - 0
src/pages/SmartOps/components/WorkAnalysis.less

@@ -0,0 +1,43 @@
+.box {
+  border-radius: 5px;
+  padding-bottom: 5px;
+  margin-bottom: 25px;
+}
+.title {
+  font-size: 30px;
+  color: #4a90e2;
+  line-height: 40px;
+  padding-left: 30px;
+  padding-top: 36px;
+}
+.list {
+  margin: 0;
+  padding: 0;
+  padding-left: 10px;
+  li {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    font-size: 26px;
+    border-bottom: 1px solid #dbdbdb;
+    padding: 20px 0;
+    padding-left: 38px;
+    &:last-child {
+      border-bottom: none;
+    }
+  }
+  .listTitle {
+    color: #4a4a4a;
+  }
+  .btn {
+    cursor: pointer;
+    display: flex;
+    align-items: center;
+    line-height: 1;
+  }
+  .icon {
+    color: #fff;
+    margin-left: 20px;
+    font-size: 24px;
+  }
+}

+ 373 - 0
src/pages/SmartOps/index.js

@@ -0,0 +1,373 @@
+import PageContent from '@/components/PageContent';
+import PageTitle from '@/components/PageTitle';
+import TabsContent from '@/components/TabsContent';
+import {
+  getHistoryRecord,
+  queryProjectConfig,
+  querySimulationProfit,
+} from '@/services/SmartOps';
+import { getPatrolPerception } from '@/services/dumu';
+import { GetTokenFromUrl, UnityAction } from '@/utils/utils';
+import {
+  connect,
+  useNavigate,
+  useParams,
+  useRequest,
+  useSearchParams,
+} from '@umijs/max';
+import { Tabs } from 'antd';
+import dayjs from 'dayjs';
+import { useEffect, useMemo, useRef, useState } from 'react';
+import Analysis from './Analysis';
+import DeviceAnalysis from './components/DeviceAnalysis';
+import VideoAnalysis from './components/VideoAnalysis';
+import WorkAnalysis from './components/WorkAnalysis';
+import styles from './index.less';
+
+const { TabPane } = Tabs;
+const icon06 = require('@/assets/smartOps/icon06.png');
+const icon07 = require('@/assets/smartOps/icon07.png');
+
+let timer = '';
+
+function SmartOps(props) {
+  const { list, dispatch, loading, loadingDev, autoReport } = props;
+
+  const navigate = useNavigate();
+  const { projectId } = useParams();
+  const [searchParams, setSearchParams] = useSearchParams();
+  const time = searchParams.get('time');
+  const idList = searchParams.get('idList');
+  const patrolId = searchParams.get('patrolId');
+
+  const [tab, setTab] = useState('1');
+  const [videoNum, setVideoNum] = useState(0);
+
+  const signalRef = useRef();
+
+  const {
+    data: reportData,
+    loading: reportLoading,
+    run: getReportData,
+  } = useRequest(
+    () =>
+      getHistoryRecord(
+        convertObject2FormData({ project_id: Number(projectId) }),
+      ),
+    {
+      manual: true,
+      formatResult: (res) => {
+        let data = res.data.list[0];
+        data.Num2Length = data.Num2.split(',').length;
+        return data;
+      },
+    },
+  );
+
+  const [sTime, eTime] = useMemo(() => {
+    let sTime = null,
+      eTime = null;
+    if (time) {
+      eTime = time;
+      sTime = dayjs(eTime).subtract(10, 'minute').format('YYYY-MM-DD HH:mm:ss');
+    } else if (reportData) {
+      eTime = reportData.CTime;
+      sTime = dayjs(eTime).subtract(10, 'minute').format('YYYY-MM-DD HH:mm:ss');
+    }
+    return [sTime, eTime];
+  }, [reportData]);
+
+  const initDate = () => {
+    if (idList || reportData?.Num2) {
+      //工艺分析
+      dispatch({
+        type: 'smartOps/queryList',
+        payload: {
+          project_id: projectId * 1,
+          projectId: projectId * 1,
+          ids: idList || reportData.Num2,
+          page_size: 999,
+          isNewRole: [46, 65, 92, 94].includes(projectId * 1),
+        },
+        callback: (data) => {
+          UnityAction.sendMsg('ProcessAnalysis', JSON.stringify(data?.list));
+        },
+      });
+    }
+
+    dispatch({
+      type: 'eqSelfInspection/getPatrolDataById',
+      payload: {
+        routeId: patrolId || reportData.PatrolId,
+      },
+      callback: (data) => {
+        // console.log(data);
+        const message = data?.extendWarningAllData?.map((item) => {
+          return {
+            DeviceCode: item.DeviceCode,
+            grade_alarm: item.Status === 2 || item.Status === 1 ? 1 : 2,
+          };
+        });
+        UnityAction.sendMsg('DevAnalysis', JSON.stringify(message));
+      },
+    });
+  };
+
+  const handlerHistoryClick = () => {
+    navigate(
+      `/smart-ops/history-record/${projectId}?JWT-TOKEN=${GetTokenFromUrl()}`,
+    );
+  };
+  const handlerRecordClick = () => {
+    navigate(
+      `/smart-ops/operation-record/${projectId}?JWT-TOKEN=${GetTokenFromUrl()}`,
+    );
+  };
+
+  // 工况分析
+  const {
+    data: workAnalysisRequest,
+    run: runWork,
+    loading: loadingWork,
+  } = useRequest(
+    () =>
+      queryProjectConfig({
+        project_id: projectId,
+        s_time: sTime,
+        e_time: eTime,
+      }),
+    {
+      manual: true,
+      onSuccess(res) {
+        if (!res) return;
+        UnityAction.sendMsg(
+          'WorkAnalysis',
+          JSON.stringify(res.project_categorys),
+        );
+      },
+    },
+  );
+
+  const optimizationNumber = workAnalysisRequest?.optimizationNumber || 0;
+
+  //感知分析
+  const {
+    data: videoData,
+    loading: loadingVideo,
+    run: runVideo,
+  } = useRequest(
+    () =>
+      getPatrolPerception({
+        project_id: projectId * 1,
+        patrol_id: patrolId || reportData.PatrolId,
+        s_time: sTime,
+        e_time: eTime,
+      }),
+    {
+      manual: true,
+      onSuccess: (data) => {
+        const count1 = data.dumu_list?.length || 0;
+        const count2 =
+          data.environment_list?.filter((item) => item.status)?.length || 0;
+        const count3 =
+          data.fluid_level_list?.filter((item) => item.status)?.length || 0;
+        setVideoNum(count1 + count2 + count3);
+        const Cam =
+          data.dumu_list?.map((item) => {
+            item.key = `dumu_${item.id}`;
+            return item.device_name;
+          }) || [];
+        const Dev1 =
+          data.environment_list?.map((item, index) => {
+            item.key = `environment_${index}_${item.device_code}`;
+            return { DeviceCode: item.device_code, grade_alarm: item.status };
+          }) || [];
+        const Dev2 =
+          data.fluid_level_list?.map((item, index) => {
+            item.key = `fluid_${index}_${item.device_code}`;
+            return { DeviceCode: item.device_code, grade_alarm: item.status };
+          }) || [];
+        const params = { Cam, Dev: [...Dev1, ...Dev2] };
+        UnityAction.sendMsg('SensorAnalysis', JSON.stringify(params));
+      },
+    },
+  );
+
+  const AnalysisNumber = list?.pagenation?.total || 0;
+
+  const deviceNumber = autoReport?.extendWarningData?.length || 0;
+
+  // 利润
+  const { data: profitData, run: getProfit } = useRequest(
+    () =>
+      querySimulationProfit({
+        project_id: projectId,
+        s_time: dayjs().subtract(1, 'day').format('YYYY-MM-DD HH:00:00'),
+        e_time: dayjs().format('YYYY-MM-DD HH:00:00'),
+      }),
+    {
+      formatResult(data) {
+        if (!data?.info) return '-';
+
+        return Object.values(data.info).reduce(
+          (total, currentValue) => total + currentValue,
+          0,
+        );
+      },
+      manual: true,
+    },
+  );
+
+  const showTime = useMemo(() => {
+    if (!eTime) return '';
+    return dayjs(eTime).format('MM-DD HH:mm');
+  }, [eTime]);
+
+  const onChangeTab = (type) => {
+    setTab(type);
+    UnityAction.sendMsg('SmartAnalysisTab', type);
+  };
+
+  useEffect(() => {
+    if (!eTime) return;
+    initDate();
+    runWork();
+    runVideo();
+    getProfit();
+  }, [eTime]);
+
+  useEffect(() => {
+    if (!time) {
+      getReportData();
+      console.log('--------10分钟刷新数据--------', eTime);
+      timer = setInterval(() => {
+        getReportData();
+      }, 60000);
+    }
+
+    dispatch({
+      type: 'smartOps/queryProcessSection',
+      payload: projectId,
+    });
+
+    // 通知unity当前处于工况分析
+    UnityAction.sendMsg('SmartAnalysisTab', 1);
+
+    const controller = new AbortController();
+    signalRef.current = controller;
+
+    return () => {
+      signalRef.current.abort();
+      clearInterval(timer);
+    };
+  }, []);
+
+  return (
+    <PageContent>
+      <PageTitle returnable={time}>智慧运营</PageTitle>
+      <div className={`card-box ${styles.topContent}`}>
+        <div className={styles.titleContent}>
+          <span className={styles.time}>{showTime}</span>
+          {!time && (
+            <div style={{ display: 'flex' }}>
+              <div className={styles.iconLeft} onClick={handlerHistoryClick} />
+              <div className={styles.iconRight} onClick={handlerRecordClick} />
+            </div>
+          )}
+        </div>
+        <div className={styles.middle}>
+          <div className={styles.left}>
+            <div className={styles.in} />
+            <div className={styles.out} />
+          </div>
+          <div className={styles.right}>
+            <div className={styles.item1}>
+              工况分析:
+              {optimizationNumber > 0
+                ? `${optimizationNumber}项可优化`
+                : '暂无优化'}
+            </div>
+            <div className={styles.item2}>
+              设备分析:
+              {autoReport?.extendWarningData?.length > 0
+                ? `${autoReport?.extendWarningData?.length}项可优化`
+                : '暂无优化'}
+            </div>
+            <div className={styles.item3}>
+              工艺分析:
+              {list?.pagenation?.total > 0
+                ? `${list?.pagenation?.total}项可优化`
+                : '暂无优化'}
+            </div>
+            <div className={styles.item4}>
+              感知分析:{videoNum > 0 ? `${videoNum}项可优化` : '暂无优化'}
+            </div>
+          </div>
+        </div>
+        <div className={styles.text}>通过智慧分析预计可省{profitData}元</div>
+      </div>
+      <div className={styles.tabContent}>
+        <TabsContent
+          defaultActiveKey="1"
+          small
+          items={[
+            {
+              label: `工况分析(${loadingWork ? '-' : optimizationNumber})`,
+              key: '1',
+              children: (
+                <WorkAnalysis
+                  workAnalysisRequest={workAnalysisRequest}
+                  projectId={projectId}
+                  loading={loadingWork}
+                  eTime={eTime}
+                />
+              ),
+            },
+            {
+              label: `设备分析(${
+                loadingDev ? '-' : autoReport?.extendWarningData?.length
+              })`,
+              key: '4',
+              children: <DeviceAnalysis />,
+            },
+            {
+              label: `工艺分析(${loading ? '-' : AnalysisNumber})`,
+              key: '2',
+              children: <Analysis />,
+            },
+            {
+              label: `感知分析(${loadingVideo ? '-' : videoNum})`,
+              key: '3',
+              children: (
+                <VideoAnalysis
+                  videoNum={videoNum}
+                  videoData={videoData}
+                  loading={loadingVideo}
+                />
+              ),
+            },
+          ]}
+          onChange={onChangeTab}
+        />
+      </div>
+    </PageContent>
+  );
+}
+
+const convertObject2FormData = (params) => {
+  const formData = new FormData();
+  Object.entries(params).forEach(([key, value]) => {
+    if (value !== null && value !== undefined && value !== NaN) {
+      formData.append(key, value);
+    }
+  });
+  return formData;
+};
+
+export default connect(({ smartOps, eqSelfInspection, loading }) => ({
+  list: smartOps.list,
+  loading: loading.models.smartOps,
+  processList: smartOps.processList,
+  autoReport: eqSelfInspection.autoReport,
+  loadingDev: loading.effects['eqSelfInspection/getPatrolDataById'],
+}))(SmartOps);

+ 180 - 0
src/pages/SmartOps/index.less

@@ -0,0 +1,180 @@
+.topContent {
+  padding: 8px 12px;
+  margin: 16px 0;
+  color: #1c50b3;
+  font-size: 28px;
+  .titleContent {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    .time {
+      // font-size: 20px;
+      color: #6e6e6e;
+    }
+    .iconLeft {
+      width: 38px;
+      height: 38px;
+      background: url('@/assets/smartOps/icon06.png') no-repeat center;
+      background-size: 100% 100%;
+    }
+    .iconRight {
+      .iconLeft;
+      margin-left: 20px;
+      background: url('@/assets/smartOps/icon07.png') no-repeat center;
+      background-size: 100% 100%;
+    }
+  }
+  .middle {
+    margin: 10px 0;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    .left {
+      position: relative;
+      .in {
+        width: 212px;
+        height: 212px;
+        background: url('@/assets/smartOps/icon04.png') no-repeat center;
+        background-size: 100% 100%;
+        animation-name: scaleUp;
+        animation-duration: 5s;
+        animation-iteration-count: infinite;
+        animation-timing-function: linear;
+      }
+      .out {
+        position: absolute;
+        left: 0;
+        top: 0;
+        width: 212px;
+        height: 212px;
+        background: url('@/assets/smartOps/icon05.png') no-repeat center;
+        background-size: 100% 100%;
+        animation-name: spin;
+        animation-duration: 30s;
+        animation-iteration-count: infinite;
+        animation-timing-function: linear;
+      }
+    }
+    .right {
+      display: flex;
+      flex-direction: column;
+      align-items: flex-end;
+      .item {
+        width: 585px;
+        height: 52px;
+        white-space: nowrap;
+        margin-bottom: 10px;
+        background: url('@/assets/smartOps/icon01.png') no-repeat center;
+        background-size: 100% 100%;
+        line-height: 52px;
+      }
+      .item1 {
+        .item;
+        padding-left: 96px;
+      }
+      .item2 {
+        .item;
+        width: 527px;
+        padding-left: 40px;
+        background-image: url('@/assets/smartOps/icon02.png');
+      }
+      .item3 {
+        .item;
+        width: 527px;
+        padding-left: 40px;
+        background-image: url('@/assets/smartOps/icon03.png');
+      }
+      .item4 {
+        .item;
+        margin-bottom: 0;
+        padding-left: 96px;
+        background-image: url('@/assets/smartOps/icon08.png');
+      }
+    }
+  }
+  .text {
+    padding: 8px 20px;
+    line-height: 24px;
+    font-size: 24px;
+    color: #4a90e2;
+    // text-align: center;
+  }
+}
+
+.anaBtn {
+  font-size: 12px !important;
+  padding: 6px;
+  line-height: 12px;
+  height: auto !important;
+}
+
+@keyframes spin {
+  from {
+    transform: rotate(0deg);
+  }
+
+  to {
+    transform: rotate(360deg);
+  }
+}
+@keyframes scaleUp {
+  0% {
+    transform: translate(0px, 0px);
+  }
+  25% {
+    transform: translate(0px, -5px);
+  }
+  75% {
+    transform: translate(0px, 5px);
+  }
+  100% {
+    transform: translate(0px, 0px);
+  }
+}
+
+.marginBottom {
+  margin-bottom: 20px;
+}
+.marginLeft {
+  margin-left: 20px;
+}
+.marginRight {
+  margin-right: 20px;
+}
+.searchContent {
+  display: flex;
+  align-items: center;
+  margin-bottom: 20px;
+  color: #4a4a4a;
+  font-size: 24px;
+  white-space: nowrap;
+  margin-top: 40px;
+  :global {
+    .ant-select {
+      margin: 0 40px 0 10px;
+      // background-color: #284e83;
+    }
+  }
+}
+.timePicker {
+  // background-color: #284e83;
+}
+
+.form {
+  width: 100%;
+  margin-bottom: 14px;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+}
+
+.tableSelect {
+  background: rgba(145, 192, 238, 0.16) !important;
+}
+.tabContent {
+  :global {
+    .ant-table-tbody > tr:nth-child(even) {
+      background: inherit;
+    }
+  }
+}

+ 87 - 0
src/pages/SmartOps/models/smartOps.js

@@ -0,0 +1,87 @@
+import {
+  queryAnalysisDict,
+  queryProcessSection,
+} from '@/services/OperationManagement';
+import { queryList, queryListNew } from '@/services/diagnosticTec';
+export default {
+  namespace: 'smartOps',
+
+  state: {
+    list: [],
+    processList: [],
+  },
+
+  effects: {
+    *queryList({ payload, callback }, { call, put, select }) {
+      if (!payload.ids) {
+        yield put({
+          type: 'save',
+          payload: {
+            list: {
+              list: [],
+              pagenation: { total: 0 },
+            },
+          },
+        });
+        return;
+      }
+      if (payload.isNewRole) {
+        const analysisDict = yield call(queryAnalysisDict, { pageSize: 9999 });
+        const response = yield call(queryListNew, payload);
+        if (response) {
+          response?.data?.list?.map((item,index) => {
+            item.index = index;
+            var reason = analysisDict?.data?.find(
+              (reason) => reason.id == item.Reason,
+            );
+            if (reason) {
+              item.Reason = reason.content;
+            }
+            var FixPlanArr = item.FixPlan.split(',');
+            if (FixPlanArr.length > 0) {
+              item.FixPlan = [];
+              FixPlanArr.map((fixItem) => {
+                var fixPlan = analysisDict?.data?.find(
+                  (reason) => reason.id == fixItem,
+                );
+                if (fixPlan) item.FixPlan.push(fixPlan);
+              });
+            }
+          });
+          yield put({
+            type: 'save',
+            payload: { list: response.data },
+          });
+          callback && callback?.(response.data);
+        }
+      } else {
+        const response = yield call(queryList, payload);
+        if (response) {
+          yield put({
+            type: 'save',
+            payload: { list: response.data },
+          });
+        }
+        callback && callback?.(response.data);
+      }
+    },
+    *queryProcessSection({ payload }, { call, put }) {
+      const list = yield call(queryProcessSection, payload);
+      if (list) {
+        yield put({
+          type: 'save',
+          payload: { processList: list.data },
+        });
+      }
+    },
+  },
+
+  reducers: {
+    save(state, action) {
+      return {
+        ...state,
+        ...action.payload,
+      };
+    },
+  },
+};

+ 0 - 0
src/pages/SmartReport/components/EqSelfPie.js


+ 465 - 0
src/pages/SmartReport/index.js

@@ -0,0 +1,465 @@
+import ModuleTitle from '@/components/ManagementPage/moduleTitle';
+import PageContent from '@/components/PageContent';
+import { queryWorkReport } from '@/services/smartReport';
+import { LeftOutlined } from '@ant-design/icons';
+import { useNavigate, useParams, useRequest } from '@umijs/max';
+import { ConfigProvider, DatePicker, Select, Spin } from 'antd';
+import zhCN from 'antd/es/locale/zh_CN';
+import dayjs from 'dayjs';
+import * as echarts from 'echarts';
+import { useEffect, useRef, useState } from 'react';
+import styles from './index.less';
+
+const { RangePicker } = DatePicker;
+
+const SmartReport = () => {
+  const navigate = useNavigate();
+  const { projectId } = useParams();
+  const FORMAT = 'YYYY-MM-DD';
+
+  const [showRange, setShowRange] = useState(false);
+  const [date, setDate] = useState({
+    stime: dayjs().subtract(8, 'day').format(FORMAT),
+    etime: dayjs().subtract(1, 'day').format(FORMAT),
+  });
+
+  const eqDomRef = useRef(null);
+  const eqChartRef = useRef(null);
+
+  const taskDomRef = useRef(null);
+  const taskChartRef = useRef(null);
+
+  const workDomRef = useRef(null);
+  const workChartRef = useRef(null);
+
+  const workScoreDomRef = useRef(null);
+  const workScoreChartRef = useRef(null);
+
+  // const defaultData = {
+  //   electricity: '1111',
+  //   in_water: '22222',
+  //   maintain_record: '333',
+  //   medicine: '444',
+  //   out_water: '666',
+  //   push_complete_task: '777',
+  //   push_complete_task_per: '888',
+  //   push_task: '555',
+  //   repair_record: '999',
+  //   self_inspection_abnormal_task: '1212',
+  //   self_inspection_normal_task: '13123',
+  //   self_inspection_task: '1414',
+  //   water_electricity: '1515',
+  //   water_medicine: '1616',
+  //   work_order_complete_task: '1717',
+  //   work_order_complete_task_per: '80',
+  //   work_order_task: '123',
+  //   patrol_safe: 2,
+  //   patrol_section: 3,
+  //   push_optimize_task: 6,
+  //   push_complete_task: 4,
+  //   eqData: [
+  //     { name: '安全隐患', value: 2 },
+  //     { name: '工艺异常', value: 3 },
+  //     { name: '设备异常', value: 4 },
+  //   ],
+  //   mandate_type_con: 1,
+  //   mandate_type_group: 2,
+  //   mandate_type_pat: 3,
+  //   mandate_type_pro: 4,
+  //   taskData: [
+  //     { name: '工况任务', value: 2 },
+  //     { name: '集团任务', value: 3 },
+  //     { name: '系统自检', value: 4 },
+  //     { name: '生产任务', value: 5 },
+  //   ],
+  //   chart_check: 1,
+  //   chart_part: 2,
+  //   chart_section: 3,
+  //   repair_record: 4,
+  //   maintain_record: 5,
+  //   patrol_mandate_record: 6,
+  //   reagent_record: 7,
+  //   workOrderData: [
+  //     { name: '盘点', value: 2 },
+  //     { name: '备品备件', value: 3 },
+  //     { name: '工艺工单', value: 4 },
+  //     { name: '维修工单', value: 5 },
+  //     { name: '保养工单', value: 5 },
+  //     { name: '巡检工单', value: 5 },
+  //     { name: '加药工单', value: 5 },
+  //   ],
+  //   con_level_five: 6, //较差
+  //   con_level_four: 1, //一般
+  //   con_level_one: 2, //优秀
+  //   con_level_three: 3, //较好
+  //   con_level_two: 4, //良好
+  //   workScoreData: [
+  //     { name: '较差', value: 2 },
+  //     { name: '一般', value: 3 },
+  //     { name: '优秀', value: 4 },
+  //     { name: '较好', value: 5 },
+  //     { name: '良好', value: 4 },
+  //   ],
+  // };
+
+  const {
+    data = {},
+    run,
+    loading,
+  } = useRequest(
+    (date) =>
+      queryWorkReport({
+        project_id: projectId,
+        ...date,
+      }),
+    {
+      defaultParams: [{ project_id: projectId, ...date }],
+      formatResult: (res) => {
+        const data = res.data;
+        return {
+          ...data,
+          eqData: [
+            { name: '安全隐患', value: data.patrol_safe },
+            { name: '工艺异常', value: data.patrol_section },
+            { name: '设备异常', value: data.push_complete_task },
+          ],
+          taskData: [
+            { name: '工况任务', value: data.mandate_type_con },
+            { name: '集团任务', value: data.mandate_type_group },
+            { name: '系统自检', value: data.mandate_type_pat },
+            { name: '生产任务', value: data.mandate_type_pro },
+          ],
+          workOrderData: [
+            { name: '盘点', value: data.chart_check },
+            { name: '备品备件', value: data.chart_part },
+            { name: '工艺工单', value: data.chart_section },
+            { name: '维修工单', value: data.repair_record },
+            { name: '保养工单', value: data.maintain_record },
+            { name: '巡检工单', value: data.patrol_mandate_record },
+            { name: '加药工单', value: data.reagent_record },
+          ],
+          workScoreData: [
+            { name: '较差', value: data.con_level_five },
+            { name: '一般', value: data.con_level_four },
+            { name: '优秀', value: data.con_level_one },
+            { name: '较好', value: data.con_level_three },
+            { name: '良好', value: data.con_level_two },
+          ],
+        };
+      },
+    },
+  );
+
+  const handleChange = (value) => {
+    const date = { ...date };
+    switch (value) {
+      case '1':
+        date.stime = dayjs().subtract(8, 'day').format(FORMAT);
+        date.etime = dayjs().subtract(1, 'day').format(FORMAT);
+        setDate(date);
+        setShowRange(false);
+        run(date);
+        break;
+      case '2':
+        date.stime = dayjs().subtract(1, 'month').format(FORMAT);
+        date.etime = dayjs().subtract(1, 'day').format(FORMAT);
+        setDate(date);
+        setShowRange(false);
+        run(date);
+        break;
+      case '3':
+        setShowRange(true);
+        break;
+    }
+  };
+
+  const {
+    ele_65,
+    ele_66,
+    electricity,
+    in_water,
+    maintain_record,
+    medicine,
+    out_water,
+    push_optimize_task,
+    push_complete_task,
+    push_complete_task_per,
+    push_task,
+    repair_record,
+    self_inspection_abnormal_task,
+    self_inspection_normal_task,
+    self_inspection_task,
+    water_electricity,
+    water_medicine,
+    work_order_complete_task,
+    work_order_complete_task_per,
+    work_order_task,
+  } = data;
+
+  useEffect(() => {
+    initData();
+  }, [data]);
+
+  useEffect(() => {
+    eqChartRef.current = echarts.init(eqDomRef.current);
+    taskChartRef.current = echarts.init(taskDomRef.current);
+    workChartRef.current = echarts.init(workDomRef.current);
+    workScoreChartRef.current = echarts.init(workScoreDomRef.current);
+
+    return () => {
+      eqChartRef.current.dispose();
+      taskChartRef.current.dispose();
+      workChartRef.current.dispose();
+      workScoreChartRef.current.dispose();
+    };
+  }, []);
+
+  const initData = () => {
+    const eqOption = getPieOption(data.eqData, '异常类型统计');
+    if (eqChartRef.current) eqChartRef.current.setOption(eqOption);
+
+    const taskOption = getPieOption(data.taskData, '任务类型统计');
+    if (taskChartRef.current) taskChartRef.current.setOption(taskOption);
+
+    const workOption = getPieOption(data.workOrderData, '工况评估统计');
+    if (workChartRef.current) workChartRef.current.setOption(workOption);
+
+    const workScoreOption = getPieOption(data.workScoreData, '工况评估统计');
+    if (workScoreChartRef.current)
+      workScoreChartRef.current.setOption(workScoreOption);
+  };
+
+  const onChange = (value) => {
+    if (!value) return;
+    const date = { ...date };
+    date.stime = value[0].format(FORMAT);
+    date.etime = value[1].format(FORMAT);
+    run(date);
+    setDate(date);
+  };
+
+  const handleOnClick = () => {
+    history.back();
+  };
+
+  return (
+    <PageContent closeable={false}>
+      <ConfigProvider locale={zhCN}>
+        <div className={styles.head}>
+          <div className={styles.nameContainer}>
+            <LeftOutlined
+              onClick={handleOnClick}
+              style={{ fontSize: 36, cursor: 'pointer', marginRight: '20px' }}
+            />
+            <div>
+              <div className={styles.name}>智慧运营报告</div>
+              <div className={styles.photo}>
+                {dayjs(date.stime).format('MM月DD日')}-
+                {dayjs(date.etime).format('MM月DD日')}
+              </div>
+            </div>
+          </div>
+          <div className={styles.headRight}>
+            <div>
+              时间:
+              <Select
+                className={styles.headRightSelect}
+                defaultValue="1"
+                style={{ width: 180 }}
+                onChange={handleChange}
+                popupClassName={styles.headRightSelect}
+                options={[
+                  {
+                    value: '1',
+                    label: '近7天',
+                  },
+                  {
+                    value: '2',
+                    label: '近30天',
+                  },
+                  {
+                    value: '3',
+                    label: '自定义时间',
+                  },
+                ]}
+              />
+            </div>
+            <div>
+              {showRange && <RangePicker inputReadOnly onChange={onChange} />}
+            </div>
+          </div>
+        </div>
+        <Spin spinning={loading}>
+          <div className={styles.box}>
+            <div className={styles.left}>概览</div>
+            <div className={styles.title}>无锡锡山水厂</div>
+            <div className={styles.content}>
+              {/* <div className={styles.item}>智慧运营天数:{}天</div>
+          <div className={styles.item}>智慧运营节省金额:{}元</div> */}
+              <div className={styles.item}>累计进水:{in_water}</div>
+              <div className={styles.item}>累计出水:{out_water}</div>
+              <div className={styles.item}>吨水能耗:{water_electricity}</div>
+              <div className={styles.item}>吨水药耗:{water_medicine}元</div>
+              <div className={styles.item}>
+                系统自检次数:{self_inspection_task}次
+              </div>
+              <div className={styles.item}>
+                优化建议: {push_optimize_task}条
+              </div>
+              <div className={styles.item}>
+                任务完成:{push_complete_task}个
+              </div>
+              <div className={styles.item}>
+                工单完成:{work_order_complete_task}个
+              </div>
+              <div className={styles.item}>设备维修:{repair_record}个</div>
+              <div className={styles.item}>设备保养:{maintain_record}个</div>
+            </div>
+          </div>
+          <div className={styles.box}>
+            <ModuleTitle title="水量" />
+            <div className={styles.content}>
+              <div className={styles.item}>累计进水:{in_water}</div>
+              <div className={styles.item}>累计出水:{out_water}</div>
+            </div>
+          </div>
+          <div className={styles.box}>
+            <ModuleTitle title="能耗" />
+            <div className={styles.content}>
+              <div className={styles.item}>吨水能耗:{water_electricity}</div>
+              <div className={styles.item}>累计耗能量:{electricity}</div>
+            </div>
+          </div>
+          <div className={styles.box}>
+            <ModuleTitle title="药耗" />
+            <div className={styles.content}>
+              <div className={styles.item}>吨水药耗:{water_medicine}</div>
+              <div className={styles.item}>累计用药量:{medicine}</div>
+            </div>
+          </div>
+          <div className={styles.box}>
+            <ModuleTitle title="系统自检" />
+            <div className={styles.threeContent}>
+              <div className={styles.numItem}>
+                <div>{self_inspection_task}</div>
+                <div>自检次数</div>
+              </div>
+              <div className={styles.numItem}>
+                <div>{self_inspection_abnormal_task}</div>
+                <div>异常次数</div>
+              </div>
+              <div className={styles.numItem}>
+                <div>{self_inspection_normal_task}</div>
+                <div>正常次数</div>
+              </div>
+            </div>
+            <div
+              ref={eqDomRef}
+              style={{ height: '340px', margin: '10px 0 10px 0' }}
+            ></div>
+            {/* <div>异常类型统计(设备异常、工艺异常、安全隐患异常)</div> */}
+          </div>
+          <div className={styles.box}>
+            <ModuleTitle title="智慧运营" />
+            <div style={{ padding: '20px 20px 0 20px' }}>
+              <div className={styles.smartText}>
+                优化条数:{push_optimize_task}条
+              </div>
+              <div className={styles.smartText}>超滤能耗:{ele_65}</div>
+              <div className={styles.smartText}>反渗透能耗:{ele_66}</div>
+            </div>
+            <div
+              ref={workScoreDomRef}
+              style={{ height: '340px', margin: '10px 0 10px 0' }}
+            ></div>
+          </div>
+          <div className={styles.box}>
+            <ModuleTitle title="任务工单" />
+            <div className={styles.content}>
+              <div className={styles.item}>任务数量:{push_task}</div>
+              <div className={styles.item}>工单数量:{work_order_task}</div>
+              <div className={styles.item}>
+                任务完成数量:{push_complete_task}
+              </div>
+              <div className={styles.item}>
+                工单完成数量:{work_order_complete_task}
+              </div>
+              <div className={styles.item}>
+                任务完成率:{Number(push_complete_task_per)}%
+              </div>
+              <div className={styles.item}>
+                工单完成率:{Number(work_order_complete_task_per)}%
+              </div>
+              <div className={styles.item}>
+                <div ref={taskDomRef} style={{ height: '400px' }}></div>
+              </div>
+              <div className={styles.item}>
+                <div ref={workDomRef} style={{ height: '400px' }}></div>
+              </div>
+            </div>
+          </div>
+          <div className={styles.box}>
+            <ModuleTitle title="设备维修保养" />
+            <div className={styles.content}>
+              <div className={styles.item}>维修数量:{repair_record}</div>
+              <div className={styles.item}>保养数量:{maintain_record}</div>
+            </div>
+          </div>
+        </Spin>
+      </ConfigProvider>
+    </PageContent>
+  );
+};
+export default SmartReport;
+const getPieOption = (chartData, name) => {
+  const option = {
+    title: {
+      text: name,
+      top: '92%',
+      left: '50%',
+      textAlign: 'center',
+      textStyle: {
+        color: '#000000',
+        fontWeight: 'normal',
+        fontSize: 18,
+      },
+    },
+    color: [
+      '#5470c6',
+      '#91cc75',
+      '#fac858',
+      '#ee6666',
+      '#73c0de',
+      '#3ba272',
+      '#fc8452',
+      '#9a60b4',
+      '#ea7ccc',
+    ],
+    tooltip: {
+      trigger: 'item',
+    },
+    legend: {
+      orient: 'horizontal',
+      // left: 'left',
+      textStyle: {
+        color: '#000000',
+        fontSize: 18,
+      },
+    },
+    series: [
+      {
+        type: 'pie',
+        radius: '50%',
+        data: chartData,
+        emphasis: {
+          itemStyle: {
+            shadowBlur: 10,
+            shadowOffsetX: 0,
+            shadowColor: 'rgba(0, 0, 0, 0.5)',
+          },
+        },
+      },
+    ],
+  };
+  return option;
+};

+ 96 - 0
src/pages/SmartReport/index.less

@@ -0,0 +1,96 @@
+.page {
+  margin: auto;
+  padding: 0 100px 100px;
+  width: 80%;
+  background: rgba(255, 255, 255, 0.78);
+  box-shadow: 0px 0px 8px 4px rgba(212, 212, 212, 0.5);
+  border-radius: 20px;
+  font-size: 30px;
+  font-weight: 400;
+  color: #4a4a4a;
+}
+
+.nameContainer {
+  display: flex;
+  justify-content: center;
+}
+
+.name {
+  font-size: 34px;
+  font-family: PingFangSC, PingFang SC;
+  font-weight: 400;
+  color: #4a4a4a;
+  line-height: 48px;
+}
+.photo {
+  font-size: 24px;
+}
+
+.head {
+  padding-left: 10px;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+
+  .time {
+    font-size: 22px;
+    font-family: PingFangSC, PingFang SC;
+    font-weight: 400;
+    color: #4a4a4a;
+    line-height: 40px;
+  }
+  .headRight {
+    font-size: 26px;
+    display: flex;
+    flex-direction: column;
+    align-items: flex-end;
+  }
+}
+
+.box {
+  margin: 24px 0;
+  padding: 38px 30px;
+  background-color: #e7f1fe;
+  .title {
+    .name;
+    width: 100%;
+    text-align: center;
+  }
+  .left {
+    position: relative;
+    left: 20px;
+    float: left;
+    font-size: 30px;
+    font-family: PingFangSC, PingFang SC;
+    font-weight: 400;
+    color: #4a4a4a;
+    line-height: 42px;
+  }
+  .content {
+    display: flex;
+    padding: 20px 20px 0 20px;
+    flex-wrap: wrap;
+    .item {
+      width: 50%;
+      margin-bottom: 18px;
+      font-size: 30px;
+      font-family: PingFangSC, PingFang SC;
+      font-weight: 400;
+      color: #4a4a4a;
+      line-height: 42px;
+    }
+  }
+  .threeContent {
+    display: flex;
+    padding: 20px;
+    justify-content: space-around;
+    .numItem {
+      .left;
+      text-align: center;
+    }
+  }
+  .smartText {
+    margin-bottom: 18px;
+    font-size: 30px;
+  }
+}

+ 87 - 0
src/pages/SystemDaily/index.js

@@ -0,0 +1,87 @@
+import PageContent from '@/components/PageContent';
+import { queryDailyWorkReport } from '@/services/user';
+import { LeftOutlined } from '@ant-design/icons';
+import { useParams, useRequest } from '@umijs/max';
+import { Spin } from 'antd';
+import dayjs from 'dayjs';
+import styles from './index.less';
+const SystemDaily = (props) => {
+  const { projectId } = useParams();
+
+  const { data = {}, loading } = useRequest(queryDailyWorkReport, {
+    defaultParams: [projectId],
+    onSuccess: (res) => {
+      const data = res.data;
+      const result = {
+        ...data,
+        task_percent:
+          data.push_task !== 0 ? data.push_complete_task / data.push_task : 0,
+        work_percent:
+          data.work_order_task !== 0
+            ? data.work_order_complete_task / data.work_order_task
+            : 0,
+      };
+      return result;
+    },
+  });
+  const {
+    automatic_task = 0,
+    push_optimize_task = 0,
+    self_inspection_task = 0,
+    self_inspection_normal_task = 0,
+    self_inspection_abnormal_task = 0,
+    push_task = 0,
+    task_percent = 0,
+    work_order_task = 0,
+    work_percent = 0,
+    user_name = '',
+    user_name_count = 0,
+  } = data;
+  const handleOnClick = () => {
+    history.back();
+  };
+  return (
+    <PageContent closeable={false}>
+      <Spin spinning={loading}>
+        <div className={styles.titleContent}>
+          <div className={styles.title}>
+            <LeftOutlined
+              onClick={handleOnClick}
+              style={{ fontSize: 36, cursor: 'pointer', marginRight: '20px' }}
+            />
+            系统工作日报
+          </div>
+          <div className={styles.time}>{dayjs().format('MM月DD日')}</div>
+        </div>
+        <div className={styles.content}>
+          <div className={styles.text}>
+            执行自控指令次数:{automatic_task}次
+          </div>
+          <div className={styles.text}>
+            推送优化建议:{push_optimize_task}条
+          </div>
+          <div className={styles.text}>
+            系统自检次数:{self_inspection_task}条
+            &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;正常次数:
+            {self_inspection_normal_task}条
+            &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;异常次数:
+            {self_inspection_abnormal_task}条
+          </div>
+          <div className={styles.text}>
+            推送任务:{push_task}条&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
+            任务完成率:{task_percent}%
+          </div>
+          <div className={styles.text}>
+            工单数量:{work_order_task}条
+            &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;工单完成率:
+            {work_percent}%
+          </div>
+          <div className={styles.text}>
+            工单完成人员第一名:{user_name}完成{user_name_count}个工单
+          </div>
+        </div>
+      </Spin>
+    </PageContent>
+  );
+};
+export default SystemDaily;

+ 38 - 0
src/pages/SystemDaily/index.less

@@ -0,0 +1,38 @@
+.main {
+  padding: 50px;
+  width: 90%;
+  margin: 60px auto;
+  background-color: #ffffff;
+  background: rgba(255, 255, 255);
+  box-shadow: 0px 0px 8px 4px rgba(212, 212, 212, 0.5);
+  border-radius: 20px;
+}
+.titleContent {
+  width: 100%;
+  // text-align: center;
+  font-family: PingFangSC, PingFang SC;
+  color: #4a4a4a;
+  margin: auto;
+  .title {
+    font-size: 34px;
+    line-height: 48px;
+  }
+  .time {
+    margin-left: 60px;
+    font-size: 22px;
+    line-height: 30px;
+  }
+}
+.content {
+  padding: 60px 30px;
+  margin: 20px auto;
+  box-shadow: 0px 0px 8px 4px rgba(212, 212, 212, 0.5);
+  border-radius: 20px;
+  background: #e7f1fe;
+  .text {
+    margin-bottom: 34px;
+    font-family: PingFangSC, PingFang SC;
+    color: #4a4a4a;
+    font-size: 30px;
+  }
+}

+ 113 - 111
src/pages/TaskManage/Detail/TaskDetail/TaskDetail.tsx

@@ -183,9 +183,7 @@ function TaskDetail(props: IPropsType) {
         tempMandate.MandateClass.value === 7
       ) {
         const image = await getDiagnosticDetail(tempMandate.ExtendId);
-        if (image?.event_bg) {
-          tempMandate.img = base64ToImageUrl(image.event_bg);
-        }
+        tempMandate.img = image.path;
       }
       setMandateDetail(tempMandate);
       setHandledWorkOrder(tempOrder);
@@ -218,7 +216,7 @@ function TaskDetail(props: IPropsType) {
         },
       });
       dataSource.push(
-        ...Object.entries(JSON.parse(mandateChild[0].Content)).map(
+        ...Object.entries(JSON.parse(mandateChild[0].Payload)).map(
           (item: any) => {
             const [key, value] = item;
             return {
@@ -258,7 +256,6 @@ function TaskDetail(props: IPropsType) {
     orderType: number,
     mandateClass: number,
   ) => {
-    console.log(mandateDetail);
     navigate(
       `/task-manage/list/order-detail?project_id=${project_id}&order_id=${orderID}&order_type=${orderType}&mandate_class=${mandateClass}`,
     );
@@ -267,125 +264,130 @@ function TaskDetail(props: IPropsType) {
   return (
     <PageContent closeable={false}>
       <PageTitle returnable>任务详情</PageTitle>
-      <div className={`${styles.cardContainer} card-box`}>
-        <div className={styles.normalInfo}>
-          <Row className={styles.infoRow} justify="space-between">
-            <Col className={styles.fontS24}>
-              时间:{mandateDetail?.CreateTime}
-            </Col>
-            {/*// @ts-ignore*/}
-            <Col className={styles.fontS24}>
-              {/*//@ts-ignore*/}
-              任务类别:{mandateDetail?.MandateClass?.label}
-            </Col>
-          </Row>
-          <Row justify="space-between">
-            <Col className={styles.fontS24}>
-              {/*//@ts-ignore*/}
-              任务状态:{mandateDetail?.Status?.label}
-            </Col>
-            <Col className={styles.fontS24}>
+      <div className=" content-title">
+        <div className={`${styles.cardContainer} card-box`}>
+          <div className={styles.normalInfo}>
+            <Row className={styles.infoRow} justify="space-between">
+              <Col className={styles.fontS24}>
+                时间:{mandateDetail?.CreateTime}
+              </Col>
               {/*// @ts-ignore*/}
-              任务负责人:{mandateDetail?.ResponsiblePeople?.CName}
-            </Col>
-          </Row>
-        </div>
-        <div className={styles.detailInfo}>
-          <Row className={styles.infoRow}>
-            <Col className={styles.fontS24} span={4}>
-              任务总结
-            </Col>
-            <Col className={styles.fontS24}>{mandateDetail?.Summary}</Col>
-          </Row>
-          {mandateDetail?.img && (
-            <Row className={styles.infoRow}>
-              <Col className={styles.fontS24} span={4}>
-                预警图片
+              <Col className={styles.fontS24}>
+                {/*//@ts-ignore*/}
+                任务类别:{mandateDetail?.MandateClass?.label}
+              </Col>
+            </Row>
+            <Row justify="space-between">
+              <Col className={styles.fontS24}>
+                {/*//@ts-ignore*/}
+                任务状态:{mandateDetail?.Status?.label}
               </Col>
               <Col className={styles.fontS24}>
-                <ReactZmage
-                  controller={{
-                    // 关闭按钮
-                    close: true,
-                    // 缩放按钮
-                    zoom: false,
-                    // 下载按钮
-                    download: false,
-                    // 翻页按钮
-                    flip: false,
-                    // 多页指示
-                    pagination: false,
-                  }}
-                  backdrop="rgba(255,255,255,0.5)"
-                  style={{ width: '350px' }}
-                  src={mandateDetail?.img}
-                />
+                {/*// @ts-ignore*/}
+                任务负责人:{mandateDetail?.ResponsiblePeople?.CName}
               </Col>
             </Row>
-          )}
-
-          {mandateDetail?.Files.length > 0 && (
+          </div>
+          <div className={styles.detailInfo}>
             <Row className={styles.infoRow}>
               <Col className={styles.fontS24} span={4}>
-                截图
+                任务总结
               </Col>
               <Col className={styles.fontS24}>
-                <ReactZmage
-                  controller={{
-                    // 关闭按钮
-                    close: true,
-                    // 缩放按钮
-                    zoom: false,
-                    // 下载按钮
-                    download: false,
-                    // 翻页按钮
-                    flip: true,
-                    // 多页指示
-                    pagination: true,
-                  }}
-                  backdrop="rgba(255,255,255,0.5)"
-                  style={{ width: '350px' }}
-                  src={mandateDetail?.Files[0].url}
-                  set={mandateDetail?.Files.map((item) => {
-                    if (item) {
-                      return {
-                        src: item.url,
-                      };
-                    }
-                    return {};
-                  })}
-                />
+                {mandateDetail?.Summary ||
+                  '根据水质相关数据.建议您调节以下参数,水厂运行可达较优状态'}
               </Col>
             </Row>
-          )}
+            {mandateDetail?.img && (
+              <Row className={styles.infoRow}>
+                <Col className={styles.fontS24} span={4}>
+                  预警图片
+                </Col>
+                <Col className={styles.fontS24}>
+                  <ReactZmage
+                    controller={{
+                      // 关闭按钮
+                      close: true,
+                      // 缩放按钮
+                      zoom: false,
+                      // 下载按钮
+                      download: false,
+                      // 翻页按钮
+                      flip: false,
+                      // 多页指示
+                      pagination: false,
+                    }}
+                    backdrop="rgba(255,255,255,0.5)"
+                    style={{ width: '350px' }}
+                    src={mandateDetail?.img}
+                  />
+                </Col>
+              </Row>
+            )}
 
-          <Row>
-            <Col className={styles.fontS24} span={4}>
-              任务内容
-            </Col>
-            <Col className={styles.fontS24} span={20}>
-              {/*{mandateDetail?.Detail}*/}
-              <Table
-                rowKey="key"
-                columns={columnDef}
-                dataSource={mandateTable}
-                pagination={false}
-              />
-            </Col>
-          </Row>
-        </div>
-        <div className={styles.relatedOrder}>
-          <Collapse
-            className={styles.collapseLabel}
-            ghost
-            expandIcon={({ isActive }) => (
-              <UpOutlined
-                style={{ color: '#5697e4' }}
-                rotate={isActive ? 180 : 0}
-              />
+            {mandateDetail?.Files.length > 0 && (
+              <Row className={styles.infoRow}>
+                <Col className={styles.fontS24} span={4}>
+                  截图
+                </Col>
+                <Col className={styles.fontS24}>
+                  <ReactZmage
+                    controller={{
+                      // 关闭按钮
+                      close: true,
+                      // 缩放按钮
+                      zoom: false,
+                      // 下载按钮
+                      download: false,
+                      // 翻页按钮
+                      flip: true,
+                      // 多页指示
+                      pagination: true,
+                    }}
+                    backdrop="rgba(255,255,255,0.5)"
+                    style={{ width: '350px' }}
+                    src={mandateDetail?.Files[0].url}
+                    set={mandateDetail?.Files.map((item) => {
+                      if (item) {
+                        return {
+                          src: item.url,
+                        };
+                      }
+                      return {};
+                    })}
+                  />
+                </Col>
+              </Row>
             )}
-            items={handledWorkOrder}
-          />
+
+            <Row>
+              <Col className={styles.fontS24} span={4}>
+                任务内容
+              </Col>
+              <Col className={styles.fontS24} span={20}>
+                {/*{mandateDetail?.Detail}*/}
+                <Table
+                  rowKey="key"
+                  columns={columnDef}
+                  dataSource={mandateTable}
+                  pagination={false}
+                />
+              </Col>
+            </Row>
+          </div>
+          <div className={styles.relatedOrder}>
+            <Collapse
+              className={styles.collapseLabel}
+              ghost
+              expandIcon={({ isActive }) => (
+                <UpOutlined
+                  style={{ color: '#5697e4' }}
+                  rotate={isActive ? 180 : 0}
+                />
+              )}
+              items={handledWorkOrder}
+            />
+          </div>
         </div>
       </div>
     </PageContent>

+ 2 - 1
src/pages/TaskManage/Detail/TaskDetail/taskDetail.types.ts

@@ -8,8 +8,9 @@ export interface IMandateChildTypes {
   ProjectId: number;
   Status: number;
   Title: string;
+  Payload: any;
 }
 
 export interface IColumn {
-  detail: {text: string, key: string};
+  detail: { text: string; key: string };
 }

+ 21 - 59
src/pages/TaskManage/Detail/TaskList/TaskList.tsx

@@ -18,11 +18,12 @@ import TopFilter from '@/pages/TaskManage/components/TopFilter';
 import { IMandateType } from '@/pages/TaskManage/index.types';
 import { useNavigate } from '@@/exports';
 import { DownOutlined } from '@ant-design/icons';
-import { Col, Collapse, CollapseProps, Divider, List, Row, Spin } from 'antd';
+import { Col, Collapse, CollapseProps, Divider, List, Row } from 'antd';
 
+import ScrollLoading from '@/components/ScrollLoading';
 import { getMandateList } from '@/services/TaskManage';
 import dayjs from 'dayjs';
-import { useEffect, useRef, useState } from 'react';
+import { useEffect, useState } from 'react';
 import styles from './taskList.less';
 
 const TaskList: React.FC<IPropsType> = (props) => {
@@ -44,16 +45,14 @@ const TaskList: React.FC<IPropsType> = (props) => {
     pageSize: 20,
     currentPage: 1,
   });
-  const [isLoadAll, setIsLoadAll] = useState(false);
-
-  const bottomAreaOfList = useRef<HTMLDivElement>(null);
+  const [pagination, setPagination] = useState({
+    current: 1,
+    total: 0,
+    pageSize: 20,
+  });
 
   const { run: getList, loading: loadData } = useRequest(getMandateList, {
-    defaultParams: [
-      {
-        ...currentParams,
-      },
-    ],
+    defaultParams: [currentParams],
     formatResult: (result) => {
       const pageInfo = result.data.pagination;
       if (result.data.pagination.current === 1) {
@@ -63,34 +62,10 @@ const TaskList: React.FC<IPropsType> = (props) => {
           setMandateList([...mandateList, ...result.data.list]);
         }
       }
-      if (pageInfo.current * pageInfo.pageSize >= pageInfo.total) {
-        setIsLoadAll(true);
-      }
-      pageInfo.current += 1;
-      setCurrentParams({
-        ...currentParams,
-        currentPage: pageInfo.current,
-      });
+      setPagination(pageInfo);
     },
   });
 
-  const handleScroll = () => {
-    if (bottomAreaOfList.current === null) {
-      return;
-    }
-    const rect = bottomAreaOfList.current.getBoundingClientRect();
-    const isVisible =
-      rect.top >= 0 &&
-      rect.left >= 0 &&
-      rect.bottom <=
-        (window.innerHeight || document.documentElement.clientHeight) &&
-      rect.right <= (window.innerWidth || document.documentElement.clientWidth);
-
-    if (isVisible) {
-      getList(currentParams);
-    }
-  };
-
   // 获取用户
   useEffect(() => {
     if (userList.length === 0) {
@@ -101,16 +76,6 @@ const TaskList: React.FC<IPropsType> = (props) => {
     }
   }, []);
 
-  // 监听滚动事件
-  useEffect(() => {
-    // 组件挂载时添加滚动事件监听
-    window.addEventListener('scroll', handleScroll);
-    // 组件卸载时移除滚动事件监听
-    return () => {
-      window.removeEventListener('scroll', handleScroll);
-    };
-  }, [loadData, currentParams]);
-
   // 配置顶部下拉过滤器
   useEffect(() => {
     const filters: ITopFilter[] = [];
@@ -157,6 +122,7 @@ const TaskList: React.FC<IPropsType> = (props) => {
       pageSize: 20,
       currentPage: 1,
     };
+
     for (let i = 0; i < value.length; i++) {
       if (value[i] !== null && topFiltersConfig[i] !== undefined) {
         params[topFiltersConfig[i].key] = value[i];
@@ -166,16 +132,6 @@ const TaskList: React.FC<IPropsType> = (props) => {
     getList(params);
   };
 
-  const loadMoreMandate =
-    !isLoadAll && !loadData ? (
-      <div
-        ref={bottomAreaOfList}
-        style={{
-          height: '1px',
-        }}
-      />
-    ) : null;
-
   const goTaskDetail = (mandate: IMandateType) => {
     navigate(
       `/task-manage/list/detail?project_id=${project_id}&mandate_id=${mandate.Id}`,
@@ -336,21 +292,27 @@ const TaskList: React.FC<IPropsType> = (props) => {
         {MandateType.find((item) => item.value === mandateType)?.label}
       </PageTitle>
       <TopFilter filters={topFiltersConfig} onChange={onTopFilterChange} />
-      <Spin spinning={loading || loadData}>
+
+      <ScrollLoading
+        height={180}
+        loading={loading || loadData}
+        pagination={pagination}
+        handleLoadData={(current: number) =>
+          getList({ ...currentParams, currentPage: current })
+        }
+      >
         <List
           itemLayout="horizontal"
           dataSource={mandateList}
           renderItem={buildTaskList}
-          loadMore={loadMoreMandate}
         />
-      </Spin>
+      </ScrollLoading>
     </PageContent>
   );
 };
 export default connect(
   ({
     taskUser,
-    mandate,
     loading,
   }: any): {
     userList: IUserType[];

+ 22 - 10
src/pages/TaskManage/Detail/TaskList/taskList.less

@@ -1,15 +1,16 @@
 .cardContainer {
   width: 100%;
   padding: 20px 20px 10px 20px;
+  margin-left: 5px;
   background-color: white;
 
   .collapseLabel {
     width: 100%;
-    .ant-collapse-arrow{
+    .ant-collapse-arrow {
       font-size: 24px;
     }
     :global {
-      .ant-collapse-item{
+      .ant-collapse-item {
         margin-bottom: 0;
       }
       .ant-collapse-header {
@@ -23,7 +24,7 @@
           flex: unset;
           margin-inline-end: unset;
         }
-        .ant-collapse-arrow{
+        .ant-collapse-arrow {
           font-size: 24px;
         }
       }
@@ -54,26 +55,37 @@
   }
 }
 
-.fontS28{
+.topContainer {
+  // height: 120px;
+  .fixedTop {
+    position: fixed;
+    top: 0;
+    width: 100%;
+    z-index: 10;
+    background-color: #ffffff;
+  }
+}
+
+.fontS28 {
   font-size: 28px;
 }
 
-.fontS26{
+.fontS26 {
   font-size: 26px;
 }
 
-.fontS24{
+.fontS24 {
   font-size: 24px;
 }
 
-.fontS20{
+.fontS20 {
   font-size: 20px;
 }
 
-.fontS18{
+.fontS18 {
   font-size: 18px;
 }
 
-.fontS16{
+.fontS16 {
   font-size: 16px;
-}
+}

+ 196 - 116
src/pages/TaskManage/Detail/TaskOrder/TaskOrder.tsx

@@ -6,21 +6,25 @@ import { OrderStatus, OrderType } from '@/pages/TaskManage/constent';
 import {
   getCraftRecordList,
   getMaintainRecordList,
+  getPatrolMandateRecord,
   getRepairRecordList,
+  getWorkOrderFlow,
+  queryReagentDetail,
 } from '@/services/TaskManage';
 import { useLocation } from '@@/exports';
 import { connect, useRequest } from '@umijs/max';
-import { Col, Row } from 'antd';
+import { Col, Row, Steps } from 'antd';
 import dayjs from 'dayjs';
 import { DefaultOptionType } from 'rc-select/es/Select';
 import React, { useEffect, useState } from 'react';
-import styles from './taskOrder.less';
-
+// @ts-ignore
 import ReactZmage from 'react-zmage';
 
+import styles from './taskOrder.less';
+
 interface IPropsType {
   userList: IUserType[];
-  dispatch: (args: { type: string; payload: object }) => {};
+  dispatch: (args: { type: string; payload: object }) => void;
 }
 
 interface IOrderInfo {
@@ -40,6 +44,11 @@ interface IOrderInfo {
   MandateImages?: any[];
 }
 
+interface IStepInfo {
+  content: string;
+  time: string;
+}
+
 const TaskOrder: React.FC<IPropsType> = (props) => {
   const { userList, dispatch } = props;
 
@@ -48,9 +57,11 @@ const TaskOrder: React.FC<IPropsType> = (props) => {
   const project_id = Number(queryParams.get('project_id'));
   const order_id = Number(queryParams.get('order_id'));
   const order_type = Number(queryParams.get('order_type'));
-  const mandate_class = Number(queryParams.get('mandate_class'));
+  // const mandate_class = Number(queryParams.get('mandate_class'));
 
   const [orderInfo, setOrderInfo] = useState<IOrderInfo>();
+  const [additionalInfo, setAdditionalInfo] = useState<any>({});
+  const [stepInfo, setStepInfo] = useState<IStepInfo[]>();
 
   // 根据type请求详情
   const { run: getMaintainDetail } = useRequest(getMaintainRecordList, {
@@ -58,9 +69,15 @@ const TaskOrder: React.FC<IPropsType> = (props) => {
     formatResult: (result) => {
       const temp = result.data.list[0];
       const tempDetail = {
-        CreateTime: dayjs(temp.CreateTime).format('YYYY-MM-DD HH:mm'),
-        PlanTime: dayjs(temp.PlanTime).format('YYYY-MM-DD HH:mm'),
-        RepairTime: dayjs(temp.RepairTime).format('YYYY-MM-DD HH:mm') || '-',
+        CreateTime: temp?.CreateTime
+          ? dayjs(temp?.CreateTime).format('YYYY-MM-DD HH:mm')
+          : '-',
+        PlanTime: temp?.PlanTime
+          ? dayjs(temp.PlanTime).format('YYYY-MM-DD HH:mm')
+          : '-',
+        RepairTime: temp?.RepairTime
+          ? dayjs(temp.RepairTime).format('YYYY-MM-DD HH:mm')
+          : '-',
         Reason: temp.Note,
         Lubrication: temp.Lubrication,
         Fasten: temp.Fasten,
@@ -79,7 +96,6 @@ const TaskOrder: React.FC<IPropsType> = (props) => {
         MandateImages:
           temp?.mandate_images?.length > 0
             ? temp.mandate_images.map((item: any) => {
-                console.log(item);
                 if (item.type === 2) {
                   return {
                     src: item.val,
@@ -104,18 +120,18 @@ const TaskOrder: React.FC<IPropsType> = (props) => {
         return;
       }
       const tempDetail: IOrderInfo = {
-        CreateTime: dayjs(temp.CreateTime).format('YYYY-MM-DD HH:mm'),
-        PlanTime: dayjs(temp.PlanTime).format('YYYY-MM-DD HH:mm'),
-        RepairTime:
-          (temp.RepairTime &&
-            dayjs(temp.RepairTime).format('YYYY-MM-DD HH:mm')) ||
-          '-',
+        CreateTime: temp.CreateTime
+          ? dayjs(temp.CreateTime).format('YYYY-MM-DD HH:mm')
+          : '-',
+        PlanTime: temp.PlanTime
+          ? dayjs(temp.PlanTime).format('YYYY-MM-DD HH:mm')
+          : '-',
+        RepairTime: temp.RepairTime
+          ? dayjs(temp.RepairTime).format('YYYY-MM-DD HH:mm')
+          : '-',
         Reason: temp.Reason,
         Repairman: userList.find((item) => item.ID === temp.Repairman) || '-',
-        DispatchMan:
-          userList.find((item) => {
-            item.ID === temp.operator_id;
-          }) || '-',
+        DispatchMan: '-',
         OrderStatus:
           OrderStatus.find((item) => item.value === temp.AcceptanceStatus) ||
           '-',
@@ -138,31 +154,6 @@ const TaskOrder: React.FC<IPropsType> = (props) => {
     },
   });
 
-  const renderReason = (detail: any) => {
-    const title = detail.match(/^(.*?)(?=\{)/g)[0];
-    const content = JSON.parse(detail.match(/\{.*\}/g)[0]);
-    console.log(content, title);
-    console.log(Object.values(content));
-    const result = [];
-    result.push(
-      <Row className={[styles.fontS28, styles.rowMargin].join(' ')}>
-        {title}
-      </Row>,
-    );
-    Object.values(content).forEach((item: any) => {
-      result.push(
-        <Row className={[styles.fontS28, styles.rowMargin].join(' ')}>
-          {item['item_alias'] +
-            ' 现有数值:' +
-            item['old_value'] +
-            ' 建议调整数值' +
-            item['new_value']}
-        </Row>,
-      );
-    });
-    return result;
-  };
-
   // 根据type请求详情
   const { run: getCraftDetail } = useRequest(getCraftRecordList, {
     manual: true,
@@ -175,10 +166,10 @@ const TaskOrder: React.FC<IPropsType> = (props) => {
           (temp.actual_end_time &&
             dayjs(temp.actual_end_time).format('YYYY-MM-DD HH:mm')) ||
           '-',
-        Reason: mandate_class !== 2 ? temp.detail : renderReason(temp.detail),
-        Repairman: userList.find((item) => item.ID === temp.checker_id) || '-',
+        Reason: temp.detail,
+        Repairman: userList.find((item) => item.ID === temp.operator_id) || '-',
         DispatchMan:
-          userList.find((item) => item.ID === temp.operator_id) || '-',
+          userList.find((item) => item.ID === temp.checker_id) || '-',
         OrderStatus:
           OrderStatus.find((item) => item.value === temp.status) || '-',
         MandateImages:
@@ -196,10 +187,76 @@ const TaskOrder: React.FC<IPropsType> = (props) => {
               })
             : [],
       };
+
       setOrderInfo(tempDetail);
     },
   });
 
+  const { run: getDosingOrder } = useRequest(queryReagentDetail, {
+    manual: true,
+    formatResult: (result) => {
+      const temp = {
+        ...result,
+        CreateTime: result?.start_time?.Valid
+          ? dayjs(result?.start_time?.Time).format('YYYY-MM-DD HH:mm')
+          : '-',
+        PlanTime: result?.plan_end_time?.Valid
+          ? dayjs(result.plan_end_time.Time).format('YYYY-MM-DD HH:mm')
+          : '-',
+        RepairTime: result?.actual_end_time?.Valid
+          ? dayjs(result.actual_end_time.Time).format('YYYY-MM-DD HH:mm')
+          : '-',
+        Reason: result.note,
+        Repairman: '-',
+        DispatchMan:
+          userList.find((user) => user.ID === result.operator_id) || '-',
+        OrderStatus:
+          OrderStatus.find((status) => status.value === result.status) || '-',
+      };
+      setOrderInfo(temp);
+      setAdditionalInfo(temp);
+    },
+  });
+
+  const { run: getPatrolOrderList } = useRequest(getPatrolMandateRecord, {
+    manual: true,
+    formatResult: (result) => {
+      if (result?.data?.list) {
+        const temp = result.data.list[0];
+        const tempDetail = {
+          ...temp,
+          CreateTime: temp?.CreatedTime
+            ? dayjs(temp.CreatedTime).format('YYYY-MM-DD HH:mm')
+            : '-',
+          PlanTime: temp.plan_end_time
+            ? dayjs(temp.plan_end_time).format('YYYY-MM-DD HH:mm')
+            : '-',
+          RepairTime: temp?.actual_end_time
+            ? dayjs(temp.actual_end_time).format('YYYY-MM-DD HH:mm')
+            : '-',
+          Reason: temp.detail,
+          Repairman: '-',
+          DispatchMan:
+            userList.find((user) => user.ID === temp.operator_id) || '-',
+          OrderStatus:
+            OrderStatus.find((status) => status.value === temp.status) || '-',
+        };
+        setOrderInfo(tempDetail);
+      }
+    },
+  });
+
+  // 获取工单流程信息
+  useRequest(getWorkOrderFlow, {
+    // manual: true,
+    defaultParams: [{ work_type: order_type, work_id: order_id }],
+    formatResult(res: IStepInfo[]) {
+      if (res && res?.length) {
+        setStepInfo(res);
+      }
+    },
+  });
+
   useEffect(() => {
     if (userList.length === 0) {
       dispatch({
@@ -210,6 +267,7 @@ const TaskOrder: React.FC<IPropsType> = (props) => {
     switch (order_type) {
       // 工艺
       case 1:
+      case 7:
       case 6:
         getCraftDetail({ project_id, work_id: order_id });
         break;
@@ -221,13 +279,47 @@ const TaskOrder: React.FC<IPropsType> = (props) => {
       case 3:
         getMaintainDetail({ project_id, id: order_id });
         break;
+      // 巡检
+      case 4:
+        getPatrolOrderList({ project_id, id: order_id });
+        break;
+      // 加药
+      case 5:
+        getDosingOrder({ id: order_id });
+        break;
     }
   }, []);
 
-  const image = [];
-  // JSON.parse(
-  //   '[{"name":"1694762306412_Screenshot_20230815_180902_uni.UNI7E61B21.jpg","path":"work_order_files/1694762306412_Screenshot_20230815_180902_uni.UNI7E61B21.jpg","created_time":"2023-09-15 15:18:29","url":"https://water-service-test.oss-cn-hangzhou.aliyuncs.com/work_order_files/1694762306412_Screenshot_20230815_180902_uni.UNI7E61B21.jpg"},{"name":"1694762306230_Screenshot_20230815_180907_uni.UNI7E61B21.jpg","path":"work_order_files/1694762306230_Screenshot_20230815_180907_uni.UNI7E61B21.jpg","created_time":"2023-09-15 15:18:29","url":"https://water-service-test.oss-cn-hangzhou.aliyuncs.com/work_order_files/1694762306230_Screenshot_20230815_180907_uni.UNI7E61B21.jpg"}]',
-  // );
+  const renderImg = () => {
+    return (
+      <Col span={18}>
+        {orderInfo?.MandateImages?.length > 0 &&
+          orderInfo?.MandateImages?.map((item, index) => {
+            return (
+              <ReactZmage
+                key={index}
+                controller={{
+                  close: true,
+                  flip: true,
+                  zoom: true,
+                  pagination: true,
+                  download: false,
+                }}
+                backdrop="rgba(255,255,255,0.5)"
+                style={{
+                  maxWidth: `${Math.floor(
+                    100 / orderInfo?.MandateImages?.length,
+                  )}%`,
+                }}
+                src={item.src}
+                set={orderInfo?.MandateImages}
+                defaultPage={index}
+              />
+            );
+          })}
+      </Col>
+    );
+  };
 
   return (
     <PageContent closeable={false}>
@@ -242,25 +334,25 @@ const TaskOrder: React.FC<IPropsType> = (props) => {
           />
           <div style={{ padding: '15px', letterSpacing: '1.5px' }}>
             <Row className={styles.rowMargin}>
-              <Col className={styles.fontS28} span={16}>
-                {/*// @ts-ignore*/}
+              <Col className={styles.fontS28} span={15}>
+                {/* @ts-ignore */}
                 工单类型:
                 {OrderType.find((item) => item.value === order_type)?.label ||
                   '-'}
               </Col>
-              <Col className={styles.fontS28} span={8}>
-                {/*// @ts-ignore*/}
+              <Col className={styles.fontS28} span={9}>
+                {/* @ts-ignore */}
                 工单负责人:{orderInfo?.Repairman?.CName || '-'}
               </Col>
             </Row>
             <Row className={styles.rowMargin}>
-              <Col className={styles.fontS28} span={16}>
-                {/*// @ts-ignore*/}
+              <Col className={styles.fontS28} span={15}>
+                {/*  @ts-ignore */}
                 工单状态:{orderInfo?.OrderStatus?.label}
               </Col>
-              <Col className={styles.fontS28} span={8}>
-                {/*// @ts-ignore*/}
-                派单人员:{orderInfo?.DispatchMan?.CName}
+              <Col className={styles.fontS28} span={9}>
+                {/* @ts-ignore */}
+                派单人员:{orderInfo?.DispatchMan?.CName || '-'}
               </Col>
             </Row>
             <Row className={styles.rowMargin}>
@@ -284,37 +376,12 @@ const TaskOrder: React.FC<IPropsType> = (props) => {
                 {orderInfo?.Reason}
               </Col>
             </Row>
+
+            {/* @ts-ignore */}
             {orderInfo?.MandateImages?.length > 0 && (
               <Row className={styles.rowMarginTop}>
                 <Col className={styles.fontS28}>任务图片:</Col>
-                <Col className={styles.fontS28} span={18}>
-                  {orderInfo?.MandateImages?.length > 0 &&
-                    orderInfo?.MandateImages?.map((item, index) => (
-                      <ReactZmage
-                        controller={{
-                          // 关闭按钮
-                          close: true,
-                          // 缩放按钮
-                          zoom: false,
-                          // 下载按钮
-                          download: false,
-                          // 翻页按钮
-                          flip: true,
-                          // 多页指示
-                          pagination: true,
-                        }}
-                        backdrop="rgba(255,255,255,0.5)"
-                        style={{
-                          maxWidth: `${Math.floor(
-                            100 / orderInfo?.MandateImages?.length,
-                          )}%`,
-                        }}
-                        src={item.src}
-                        set={orderInfo?.MandateImages}
-                        defaultPage={index}
-                      />
-                    ))}
-                </Col>
+                {renderImg()}
               </Row>
             )}
           </div>
@@ -349,34 +416,47 @@ const TaskOrder: React.FC<IPropsType> = (props) => {
           </div>
         )}
 
-        {/*<div>*/}
-        {/*  <SubTitle title="工单流程" />*/}
-        {/*  <div style={{ padding: '15px' }}>*/}
-        {/*    <Steps*/}
-        {/*      direction="vertical"*/}
-        {/*      style={{ fontSize: '14px' }}*/}
-        {/*      current={1}*/}
-        {/*      items={[*/}
-        {/*        {*/}
-        {/*          title: '工单已派遣至值班人员张**',*/}
-        {/*          description: '2023-08-02 13:23',*/}
-        {/*        },*/}
-        {/*        {*/}
-        {/*          title: '张**接收工单',*/}
-        {/*          description: '2023-08-02 13:23',*/}
-        {/*        },*/}
-        {/*        {*/}
-        {/*          title: '张**提交处理结果',*/}
-        {/*          description: '2023-08-02 13:23',*/}
-        {/*        },*/}
-        {/*        {*/}
-        {/*          title: '工单审批通过',*/}
-        {/*          description: '2023-08-02 13:23',*/}
-        {/*        },*/}
-        {/*      ]}*/}
-        {/*    />*/}
-        {/*  </div>*/}
-        {/*</div>*/}
+        {order_type === 5 && (
+          <div>
+            <SubTitle title="加药详情" />
+            <div style={{ padding: '15px' }}>
+              <Row>
+                <Col className={styles.fontS28} span={8}>
+                  药剂名称:{additionalInfo?.name || '-'}
+                </Col>
+                <Col className={styles.fontS28} span={8}>
+                  加药量:{additionalInfo?.dosage || '-'}升
+                </Col>
+                <Col className={styles.fontS28} span={8}>
+                  浓度:{additionalInfo?.concentration || '-'}
+                </Col>
+              </Row>
+            </div>
+          </div>
+        )}
+        {/* @ts-ignore */}
+        {stepInfo?.length > 0 && (
+          <div>
+            <SubTitle title="工单流程" />
+            <div style={{ padding: '15px 20px' }}>
+              <Steps
+                direction="vertical"
+                current={stepInfo?.length ? stepInfo.length - 1 : 0}
+                progressDot
+                items={stepInfo?.map((item) => {
+                  return {
+                    title: (
+                      <span className={styles.fontS28}>{item.content}</span>
+                    ),
+                    description: (
+                      <span className={styles.fontS24}>{item.time}</span>
+                    ),
+                  };
+                })}
+              />
+            </div>
+          </div>
+        )}
       </div>
     </PageContent>
   );

+ 282 - 0
src/pages/TaskManage/Detail/WorkOrderList/WorkOrderList.js

@@ -0,0 +1,282 @@
+import PageContent from '@/components/PageContent';
+import PageTitle from '@/components/PageTitle';
+import {
+  getCraftRecordList,
+  getMaintainRecordList,
+  getPatrolMandateRecord,
+  getReagentOrderList,
+  getRepairRecordList,
+} from '@/services/TaskManage';
+import { connect, useLocation, useNavigate, useRequest } from '@umijs/max';
+import { Button, Col, Empty, Row, Spin, Tabs } from 'antd';
+import dayjs from 'dayjs';
+import { useEffect, useState } from 'react';
+import { OrderStatus, OrderType } from '../../constent';
+import styles from './WorkOrderList.less';
+
+const { TabPane } = Tabs;
+
+const WorkOrderList = (props) => {
+  const { userList, dispatch } = props;
+  const location = useLocation();
+  const queryParams = new URLSearchParams(location.search);
+  const project_id = Number(queryParams.get('project_id'));
+  const order_type = Number(queryParams.get('order_type'));
+
+  const navigate = useNavigate();
+
+  useEffect(() => {
+    if (userList.length === 0) {
+      dispatch({
+        type: 'taskUser/fetchUserList',
+        payload: { project_id },
+      });
+    }
+  }, []);
+
+  const [activedStatus, setActivedStatus] = useState();
+  const [pagination, setPagination] = useState({
+    pageSize: 20,
+    currentPage: 1,
+    total: 0,
+  });
+  const [recordList, setRecordList] = useState([]);
+  const [workOrderListDOM, setWorkOrderList] = useState(null);
+
+  const goWorkOrderDetail = (order) => {
+    const orderID = order.id || order.Id;
+    navigate(
+      `/task-manage/list/order-detail?project_id=${project_id}&order_id=${orderID}&order_type=${order_type}`,
+    );
+  };
+
+  const renderTabPannal = (list) => {
+    if (list?.length === 0) {
+      setWorkOrderList(<Empty />);
+      return;
+    }
+    const tempNode = list.map((workOrder) => {
+      return (
+        <div
+          key={workOrder.id || workOrder.Id}
+          className={styles.workOrderCard}
+        >
+          <Row justify="space-between" style={{ marginBottom: '20px' }}>
+            <Col className={styles.fontS24}>{workOrder?.CreateTime}</Col>
+            <Col className={styles.fontS24}>
+              工单负责人:{workOrder?.Repairman?.CName || '-'}
+            </Col>
+          </Row>
+          <Row justify="space-between">
+            <Col className={styles.fontS24}>
+              工单状态:{workOrder?.OrderStatus?.label}
+            </Col>
+            <Col className={styles.detailBtnContainer}>
+              <Button
+                type="primary"
+                style={{ height: '40px', fontSize: '24px' }}
+                onClick={() => goWorkOrderDetail(workOrder)}
+              >
+                工单详情
+              </Button>
+            </Col>
+          </Row>
+        </div>
+      );
+    });
+    setWorkOrderList(tempNode);
+  };
+
+  const formatCreateTime = (order) => {
+    if (order_type === 1 || order_type === 6 || order_type === 7) {
+      if (order?.start_time) {
+        return dayjs(order.start_time).format('YYYY-MM-DD HH:mm');
+      }
+    } else if (order_type === 2 || order_type === 3) {
+      if (order?.CreateTime) {
+        return dayjs(order.CreateTime).format('YYYY-MM-DD HH:mm');
+      }
+    } else if (order_type === 4) {
+      if (order?.CreatedTime) {
+        return dayjs(order.CreatedTime).format('YYYY-MM-DD HH:mm');
+      }
+    } else if (order_type === 5) {
+      if (order?.start_time?.Valid) {
+        return dayjs(order.start_time.Time).format('YYYY-MM-DD HH:mm');
+      }
+    }
+    return '-';
+  };
+
+  const { run: getWorkOrderList, loading } = useRequest(
+    (status, pageInfo) => {
+      switch (order_type) {
+        case 1:
+        case 6:
+        case 7:
+          return getCraftRecordList({
+            project_id,
+            status: status || 0,
+            types: order_type === 1 ? 0 : order_type === 6 ? 1 : 2,
+            pageSize: pageInfo?.pageSize || pagination.pageSize,
+            currentPage: pageInfo?.currentPage || pagination.currentPage,
+          });
+        case 2:
+          return getRepairRecordList({
+            project_id,
+            acceptanceStatus: status || 0,
+            pageSize: pageInfo?.pageSize || pagination.pageSize,
+            currentPage: pageInfo?.currentPage || pagination.currentPage,
+          });
+        case 3:
+          return getMaintainRecordList({
+            project_id,
+            status: status || 0,
+            pageSize: pageInfo?.pageSize || pagination.pageSize,
+            currentPage: pageInfo?.currentPage || pagination.currentPage,
+          });
+        case 4:
+          return getPatrolMandateRecord({
+            project_id,
+            status: status || 0,
+            pageSize: pageInfo?.pageSize || pagination.pageSize,
+            currentPage: pageInfo?.currentPage || pagination.currentPage,
+          });
+        case 5:
+          return getReagentOrderList({
+            project_id,
+            status: status || 0,
+            pageSize: pageInfo?.pageSize || pagination.pageSize,
+            currentPage: pageInfo?.currentPage || pagination.currentPage,
+          });
+      }
+    },
+    {
+      throwOnError: true,
+      formatResult: (result) => {
+        let temp = [];
+        if (result?.data?.list) {
+          temp = result.data.list.map((item) => {
+            return {
+              ...item,
+              CreateTime: formatCreateTime(item),
+              Repairman: userList.find((user) => {
+                let temp = -1;
+                if (order_type === 2) {
+                  temp = item.Repairman;
+                } else if (order_type === 3) {
+                  temp = item.MaintenancePerson;
+                } else if (order_type === 4) {
+                  temp = '-';
+                } else if (order_type === 5) {
+                  temp = '-';
+                } else {
+                  temp = item.operator_id;
+                }
+                return temp === user.ID;
+              }),
+              OrderStatus: OrderStatus.find((status) => {
+                let temp = -1;
+                if (order_type === 2) {
+                  temp = item.AcceptanceStatus;
+                } else if (order_type === 3) {
+                  temp = item.Status;
+                } else {
+                  temp = item.status;
+                }
+                return status.value === temp;
+              }),
+            };
+          });
+        }
+
+        if (result?.data?.pagination) {
+          const tempPageInfo = {
+            pageSize:
+              result.data.pagination?.PageSize ||
+              result.data.pagination?.pageSize,
+            current:
+              result.data.pagination?.Current ||
+              result.data.pagination?.current,
+            total:
+              result.data.pagination?.Total || result.data.pagination?.total,
+          };
+          if (tempPageInfo.current === 1) {
+            setRecordList(temp);
+          } else {
+            setRecordList([...recordList, ...temp]);
+          }
+
+          if ([...recordList, ...temp].length < tempPageInfo.total) {
+            tempPageInfo.current++;
+          }
+
+          setPagination({
+            pageSize: tempPageInfo.pageSize,
+            currentPage: tempPageInfo.current,
+            total: tempPageInfo.total,
+          });
+        }
+      },
+    },
+  );
+
+  useEffect(() => {
+    renderTabPannal(recordList);
+  }, [recordList]);
+
+  const onTabChange = (key) => {
+    setActivedStatus(key);
+    setRecordList([]);
+    setPagination({ pageSize: 20, currentPage: 1, total: 0 });
+    getWorkOrderList(Number(key), { pageSize: 20, currentPage: 1, total: 0 });
+  };
+
+  return (
+    <PageContent closeable={false}>
+      <PageTitle returnable>
+        {OrderType.find((item) => item.value === order_type)?.label || ''}
+      </PageTitle>
+      <Tabs
+        size="large"
+        activeKey={activedStatus}
+        defaultActiveKey={String(OrderStatus[0].value)}
+        className={styles.identifyTab}
+        onChange={onTabChange}
+      >
+        {OrderStatus.map((item) => {
+          return (
+            <TabPane tab={item.label} key={String(item.value)}>
+              <Spin spinning={loading}>
+                <div
+                  style={{ height: 'calc(100vh - 180px)', overflowY: 'auto' }}
+                >
+                  {workOrderListDOM}
+                  <div className={styles.loadMoreBtn}>
+                    {recordList.length < pagination.total && (
+                      <Button
+                        style={{ fontSize: '24px' }}
+                        type="link"
+                        onClick={() => {
+                          getWorkOrderList(activedStatus);
+                        }}
+                      >
+                        加载更多
+                      </Button>
+                    )}
+                  </div>
+                </div>
+              </Spin>
+            </TabPane>
+          );
+        })}
+      </Tabs>
+    </PageContent>
+  );
+};
+
+export default connect(({ taskUser }) => {
+  return {
+    userList: taskUser.userList,
+  };
+})(WorkOrderList);

+ 36 - 0
src/pages/TaskManage/Detail/WorkOrderList/WorkOrderList.less

@@ -0,0 +1,36 @@
+.identifyTab :global {
+  .ant-tabs-nav-list {
+    justify-content: space-around;
+    width: 100%;
+    .ant-tabs-tab-btn {
+      font-size: 24px;
+    }
+  }
+}
+
+.workOrderCard {
+  background-color: white;
+  padding: 20px;
+  margin-top: 20px;
+  margin-bottom: 10px;
+  margin-left: 5px;
+  border-radius: 10px;
+  box-shadow: 0px 0px 8px 2px rgba(191, 191, 191, 0.2);
+}
+
+.detailBtnContainer {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+}
+
+.loadMoreBtn {
+  margin: 20px 0;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+}
+
+.fontS24 {
+  font-size: 24px;
+}

+ 394 - 0
src/pages/TaskManage/Popup/WorkOrderModal.js

@@ -0,0 +1,394 @@
+import {
+  getCraftRecordList,
+  getMaintainRecordList,
+  getReagentOrderDetail,
+  getRepairRecordList,
+  getWorkOrderFlow,
+  queryReagentDetail,
+} from '@/services/TaskManage';
+import { CloseOutlined } from '@ant-design/icons';
+import { connect, useLocation, useParams, useRequest } from '@umijs/max';
+import { Button, Col, Divider, Row, Steps } from 'antd';
+import dayjs from 'dayjs';
+import { useEffect, useState } from 'react';
+import ReactZmage from 'react-zmage';
+import SubTitle from '../components/SubTitle';
+import { OrderStatus, OrderType } from '../constent';
+import styles from './WorkOrderModal.less';
+
+import { UnityAction } from '@/utils/utils';
+
+const WorkOrderModal = (props) => {
+  const { userList, dispatch } = props;
+
+  const { projectID } = useParams();
+  const location = useLocation();
+  const queryParams = new URLSearchParams(location.search);
+  const order_id = Number(queryParams.get('order_id'));
+  const order_type = Number(queryParams.get('order_type'));
+  const mandate_class = Number(queryParams.get('mandate_class'));
+
+  const [orderInfo, setOrderInfo] = useState({});
+  const [stepInfo, setStepInfo] = useState([]);
+
+  const { run: getCraftDetail } = useRequest(getCraftRecordList, {
+    manual: true,
+    formatResult: (result) => {
+      if (result?.data?.list) {
+        const temp = result.data.list[0];
+        const tempDetail = {
+          ...temp,
+          CreateTime: dayjs(temp.start_time).format('YYYY-MM-DD HH:mm'),
+          PlanTime: dayjs(temp.plan_end_time).format('YYYY-MM-DD HH:mm'),
+          RepairTime:
+            (temp.actual_end_time &&
+              dayjs(temp.actual_end_time).format('YYYY-MM-DD HH:mm')) ||
+            '-',
+          Reason: mandate_class !== 2 ? temp.detail : renderReason(temp.detail),
+          Repairman:
+            userList.find((item) => item.ID === temp.checker_id) || '-',
+          DispatchMan:
+            userList.find((item) => item.ID === temp.operator_id) || '-',
+          OrderStatus:
+            OrderStatus.find((item) => item.value === temp.status) || '-',
+          MandateImages:
+            temp?.mandate_images?.length > 0
+              ? temp.mandate_images.map((item) => {
+                  console.log(item);
+                  if (item.type === 2) {
+                    return {
+                      src: item.val,
+                    };
+                  }
+                  return {
+                    src: `data:image/png;base64,${item.val}`,
+                  };
+                })
+              : [],
+        };
+        setOrderInfo(tempDetail);
+        // 根据不同的工单类型查询工单操作详情
+      }
+    },
+  });
+
+  const { run: getRepairDetail } = useRequest(getRepairRecordList, {
+    manual: true,
+    formatResult: (result) => {
+      if (result?.data?.list) {
+        const temp = result.data.list[0];
+
+        const tempDetail = {
+          ...temp,
+          CreateTime: dayjs(temp.CreateTime).format('YYYY-MM-DD HH:mm'),
+          PlanTime: dayjs(temp.PlanTime).format('YYYY-MM-DD HH:mm'),
+          RepairTime:
+            (temp.RepairTime &&
+              dayjs(temp.RepairTime).format('YYYY-MM-DD HH:mm')) ||
+            '-',
+          Reason: temp.Reason,
+          Repairman: userList.find((item) => item.ID === temp.Repairman) || '-',
+          DispatchMan:
+            userList.find((item) => item.ID === temp.operator_id) || '-',
+          OrderStatus:
+            OrderStatus.find((item) => item.value === temp.AcceptanceStatus) ||
+            '-',
+          MandateImages:
+            temp?.mandate_images?.length > 0
+              ? temp.mandate_images.map((item) => {
+                  if (item.type === 2) {
+                    return {
+                      src: item.val,
+                    };
+                  }
+                  return {
+                    src: `data:image/png;base64,${item.val}`,
+                  };
+                })
+              : [],
+        };
+        setOrderInfo(tempDetail);
+      }
+    },
+  });
+
+  const { run: getMaintainDetail } = useRequest(getMaintainRecordList, {
+    manual: true,
+    formatResult: (result) => {
+      if (result?.data?.list) {
+        const temp = result.data.list[0];
+        const tempDetail = {
+          ...temp,
+          CreateTime: dayjs(temp.CreateTime).format('YYYY-MM-DD HH:mm'),
+          PlanTime: dayjs(temp.PlanTime).format('YYYY-MM-DD HH:mm'),
+          RepairTime: dayjs(temp.RepairTime).format('YYYY-MM-DD HH:mm') || '-',
+          Reason: temp.Note,
+          Repairman:
+            userList.find((item) => item.ID === temp.MaintenancePerson) || '-',
+          DispatchMan:
+            userList.find(
+              (item) => (item.ID = temp.Operators[0]?.Operator?.ID),
+            ) || '-',
+          OrderStatus:
+            OrderStatus.find((item) => item.value === temp.Status) || '-',
+          MandateImages:
+            temp?.mandate_images?.length > 0
+              ? temp.mandate_images.map((item) => {
+                  if (item.type === 2) {
+                    return {
+                      src: item.val,
+                    };
+                  }
+                  return {
+                    src: `data:image/png;base64,${item.val}`,
+                  };
+                })
+              : [],
+        };
+        setOrderInfo(tempDetail);
+      }
+    },
+  });
+  const { run: getDosingOrder } = useRequest(getReagentOrderDetail, {
+    manual: true,
+    formatResult: (result) => {
+      const temp = {
+        ...result,
+        CreateTime: result?.start_time?.Valid
+          ? dayjs(result?.start_time?.Time).format('YYYY-MM-DD HH:mm')
+          : '-',
+        PlanTime: result?.plan_end_time?.Valid
+          ? dayjs(result.plan_end_time.Time).format('YYYY-MM-DD HH:mm')
+          : '-',
+        RepairTime: result?.actual_end_time?.Valid
+          ? dayjs(result.actual_end_time.Time).format('YYYY-MM-DD HH:mm')
+          : '-',
+        Reason: result.note,
+        Repairman: '-',
+        DispatchMan:
+          userList.find((user) => user.ID === result.operator_id) || '-',
+        OrderStatus:
+          OrderStatus.find((status) => status.value === result.status) || '-',
+      };
+      setOrderInfo(temp);
+      queryReagentDetail({ id: temp.id })
+        .then((res) => {
+          if (res) {
+            setAdditionalInfo(res);
+          }
+        })
+        .catch((err) => console.log(err));
+    },
+  });
+
+  useRequest(getWorkOrderFlow, {
+    defaultParams: [{ work_type: order_type, work_id: order_id }],
+    formatResult(res) {
+      if (res && res?.length) {
+        setStepInfo(res);
+      }
+    },
+  });
+
+  const closePage = () => {
+    // ToDo: send message to unity to close this page
+    UnityAction.sendMsg('CloseWorkOrder');
+  };
+
+  const renderImg = () => {
+    return (
+      <Col span={18}>
+        {orderInfo?.MandateImages?.length > 0 &&
+          orderInfo?.MandateImages?.map((item, index) => {
+            return (
+              <ReactZmage
+                key={index}
+                controller={{
+                  close: true,
+                  flip: true,
+                  zoom: true,
+                  pagination: true,
+                  download: false,
+                }}
+                backdrop="rgba(255,255,255,0.5)"
+                style={{
+                  maxWidth: `${Math.floor(
+                    100 / orderInfo?.MandateImages?.length,
+                  )}%`,
+                }}
+                src={item.src}
+                set={orderInfo?.MandateImages}
+                defaultPage={index}
+              />
+            );
+          })}
+      </Col>
+    );
+  };
+
+  useEffect(() => {
+    switch (order_type) {
+      case 1:
+      case 4:
+      case 6:
+        getCraftDetail({ project_id: projectID, work_id: order_id });
+        break;
+      case 2:
+        getRepairDetail({ project_id: projectID, id: order_id });
+        break;
+      case 3:
+        getMaintainDetail({ project_id: projectID, id: order_id });
+        break;
+      case 5:
+        getDosingOrder(order_id);
+        break;
+      default:
+        break;
+    }
+  }, []);
+
+  return (
+    <div className={styles.modalContainer}>
+      <div className={styles.modalTitle}>工单信息</div>
+      <Button
+        className={styles.closeModalBtn}
+        type="text"
+        icon={<CloseOutlined style={{ fontSize: '24px' }} />}
+        onClick={closePage}
+      />
+      <Divider style={{ margin: '0 0 30px 0' }} />
+      <div
+        style={{
+          marginTop: '20px',
+          borderRadius: '8px',
+          boxShadow: '2px 0 8px 0 rgba(0, 0, 0, 30%)',
+        }}
+      >
+        <div>
+          <SubTitle
+            title="工单信息"
+            titleFontSize={24}
+            showStatus={orderInfo?.OrderStatus?.value === 2}
+            radius
+          />
+          <div className={styles.basicalInfo}>
+            <Row>
+              <Col span={16} className={styles.fontS28}>
+                工单类型:
+                {OrderType.find((item) => item.value === order_type)?.label ||
+                  '-'}
+              </Col>
+              <Col span={8} className={styles.fontS28}>
+                {/*// @ts-ignore*/}
+                工单负责人:{orderInfo?.Repairman?.CName || '-'}
+              </Col>
+            </Row>
+            <Row>
+              <Col span={16} className={styles.fontS28}>
+                工单状态:{orderInfo?.OrderStatus?.label}
+              </Col>
+              <Col span={8} className={styles.fontS28}>
+                派单人员:{orderInfo?.DispatchMan?.CName}
+              </Col>
+            </Row>
+            <Row>
+              <Col className={styles.fontS28}>
+                派单时间:{orderInfo?.CreateTime || '-'}
+              </Col>
+            </Row>
+            <Row>
+              <Col className={styles.fontS28}>
+                计划完成时间:{orderInfo?.PlanTime || '-'}
+              </Col>
+            </Row>
+            <Row>
+              <Col className={styles.fontS28}>
+                实际完成时间:{orderInfo?.RepairTime || '-'}
+              </Col>
+            </Row>
+            {orderInfo?.MandateImages?.length > 0 && (
+              <Row>
+                <Col className={styles.fontS28} span={4}>
+                  任务图片:
+                </Col>
+                {renderImg()}
+              </Row>
+            )}
+          </div>
+        </div>
+        {order_type === 3 && (
+          <div>
+            <SubTitle title="操作内容" titleFontSize={24} />
+            <div className={styles.additionInfo}>
+              <Row justify={'space-around'}>
+                <Col className={styles.fontS28} span={8}>
+                  是否润滑/加油:{orderInfo?.Lubrication === 1 ? '是' : '否'}
+                </Col>
+                <Col className={styles.fontS28} span={8}>
+                  是否拆检:{orderInfo?.Check === 1 ? '是' : '否'}
+                </Col>
+                <Col className={styles.fontS28} span={8}>
+                  是否清洁:{orderInfo?.Clean === 1 ? '是' : '否'}
+                </Col>
+              </Row>
+              <Row justify={'space-around'}>
+                <Col className={styles.fontS28} span={8}>
+                  是否紧固:{orderInfo?.Fasten === 1 ? '是' : '否'}
+                </Col>
+                <Col className={styles.fontS28} span={8}>
+                  是否除锈:{orderInfo?.AntiCorrosive === 1 ? '是' : '否'}
+                </Col>
+                <Col className={styles.fontS28} span={8}>
+                  是否防腐:{orderInfo?.RustRemoval === 1 ? '是' : '否'}
+                </Col>
+              </Row>
+            </div>
+          </div>
+        )}
+        {order_type === 5 && (
+          <div>
+            <SubTitle title="加药详情" titleFontSize={24} />
+            <div style={{ padding: '15px' }}>
+              <Row>
+                <Col className={styles.fontS28}>加药详情:</Col>
+                <Col className={styles.fontS28} span={18}>
+                  {`药剂名称:${additionalInfo?.name || '-'} 加药量:${
+                    additionalInfo?.dosage || '-'
+                  }升 浓度:${additionalInfo?.concentration || '-'}`}
+                </Col>
+              </Row>
+            </div>
+          </div>
+        )}
+        {stepInfo.length > 0 && (
+          <div>
+            <SubTitle title="工单流程" titleFontSize={24} />
+            <div style={{ padding: '15px 20px' }}>
+              <Steps
+                direction="vertical"
+                progressDot
+                current={1}
+                items={stepInfo?.map((item) => {
+                  return {
+                    title: (
+                      <span className={styles.fontS28}>{item.content}</span>
+                    ),
+                    description: (
+                      <span className={styles.fontS20}>{item.time}</span>
+                    ),
+                  };
+                })}
+              />
+            </div>
+          </div>
+        )}
+      </div>
+    </div>
+  );
+};
+
+export default connect(({ taskUser }) => {
+  return {
+    userList: taskUser.userList,
+  };
+})(WorkOrderModal);

+ 46 - 0
src/pages/TaskManage/Popup/WorkOrderModal.less

@@ -0,0 +1,46 @@
+.modalContainer {
+  padding: 20px 30px 0px 30px;
+}
+
+.closeModalBtn {
+  position: absolute;
+  top: 30px;
+  right: 30px;
+}
+
+.modalTitle {
+  font-size: 28px;
+  font-weight: 600;
+  padding: 10px 0;
+}
+
+.basicalInfo {
+  padding: 0 15px 15px 15px;
+}
+
+.basicalInfo > * {
+  margin-top: 20px;
+}
+
+.additionInfo {
+  padding: 0 15px 15px 15px;
+}
+
+.additionInfo > * {
+  margin-top: 20px;
+}
+
+.fontS24 {
+  font-size: 24px;
+}
+.fontS26 {
+  font-size: 26px;
+}
+
+.fontS28 {
+  font-size: 28px;
+}
+
+.fontS30 {
+  font-size: 30px;
+}

+ 158 - 0
src/pages/TaskManage/Popup/index.js

@@ -0,0 +1,158 @@
+import {
+  dispatchOrder,
+  ignoreTaskRequest,
+  setTaskAutomation,
+} from '@/services/TaskManage';
+import { UnityAction } from '@/utils/utils';
+import { CloseOutlined } from '@ant-design/icons';
+import { connect, useLocation, useParams, useRequest } from '@umijs/max';
+import { Button, Divider, Tabs } from 'antd';
+import { useEffect, useState } from 'react';
+import MandateDetail from '../components/MandateDetail';
+import styles from './index.less';
+
+const TaskModal = (props) => {
+  const { projectID } = useParams();
+  const location = useLocation();
+  const queryParams = new URLSearchParams(location.search);
+
+  const mandateIDs =
+    queryParams.get('mandate_id')?.split(',').length === 1
+      ? queryParams.get('mandate_id')
+      : queryParams.get('mandate_id')?.split(',');
+
+  const { userList, dispatch } = props;
+
+  const [modalTitle, setModalTitle] = useState(null);
+  const [tabItems, setTabItems] = useState([]);
+  const [activeKey, setActiveKey] = useState();
+
+  const { run: runIgnore } = useRequest(ignoreTaskRequest, {
+    manual: true,
+  });
+
+  const { run: runDispatch } = useRequest(dispatchOrder, {
+    manual: true,
+  });
+
+  const { run: runAutomate } = useRequest(setTaskAutomation, {
+    manual: true,
+  });
+
+  const onTabChange = (key) => {
+    setActiveKey(String(key));
+  };
+
+  const closePage = () => {
+    // send message to unity close this modal page
+    UnityAction.sendMsg('CloseTask');
+  };
+  // 忽略
+  const onIgnoreTaskConfirm = async (mandateID, reason) => {
+    const params = {
+      Id: mandateID,
+      Status: 4,
+      note: reason,
+    };
+    const result = await runIgnore(params);
+    if (result) {
+      return true;
+    }
+  };
+  // 派单
+  const onDispatchTaskConfirm = async (params) => {
+    const result = await runDispatch(params);
+    if (result) {
+      return true;
+    }
+  };
+  // 自动处理
+  const onAutoHandleTaskConfirm = async (pw, mandate) => {
+    console.log(mandate);
+    const params = {
+      mandate_id: mandate.Id,
+      pw,
+    };
+    const result = runAutomate(params, mandate);
+    if (result) {
+      return true;
+    }
+  };
+
+  useEffect(() => {
+    if (mandateIDs !== undefined) {
+      if (Array.isArray(mandateIDs)) {
+        setModalTitle(null);
+        setTabItems(
+          mandateIDs.map((item, index) => {
+            return {
+              key: String(item),
+              label: <span className={styles.fontS28}>任务{index + 1}</span>,
+              children: (
+                <MandateDetail
+                  key={item}
+                  mandateID={item}
+                  userList={userList}
+                  projectID={projectID}
+                  ignoreTask={onIgnoreTaskConfirm}
+                  dispatchTask={onDispatchTaskConfirm}
+                  autoHandleTask={onAutoHandleTaskConfirm}
+                />
+              ),
+            };
+          }),
+        );
+      } else {
+        setModalTitle(<div className={styles.modalTitle}>任务详情</div>);
+      }
+    }
+  }, []);
+
+  useEffect(() => {
+    if (userList.length === 0) {
+      dispatch({
+        type: 'taskUser/fetchUserList',
+        payload: { project_id: projectID },
+      });
+    }
+  }, []);
+
+  return (
+    <div className={styles.modalContainer}>
+      <Button
+        className={styles.closeModalBtn}
+        type="text"
+        icon={<CloseOutlined style={{ fontSize: '24px' }} />}
+        onClick={closePage}
+      />
+
+      {mandateIDs && Array.isArray(mandateIDs) ? (
+        <Tabs
+          defaultActiveKey={String(mandateIDs[0])}
+          activeKey={activeKey}
+          items={tabItems}
+          onChange={onTabChange}
+        />
+      ) : (
+        <>
+          {modalTitle}
+          <Divider style={{ margin: '10px 0' }} />
+          <MandateDetail
+            mandateID={mandateIDs}
+            userList={userList}
+            projectID={projectID}
+            ignoreTask={onIgnoreTaskConfirm}
+            dispatchTask={onDispatchTaskConfirm}
+            autoHandleTask={onAutoHandleTaskConfirm}
+          />
+        </>
+      )}
+    </div>
+  );
+};
+
+export default connect(({ taskUser }) => {
+  return {
+    userList: taskUser.userList,
+  };
+})(TaskModal);

+ 43 - 0
src/pages/TaskManage/Popup/index.less

@@ -0,0 +1,43 @@
+.modalContainer {
+  padding: 30px;
+}
+
+.closeModalBtn {
+  position: absolute;
+  top: 40px;
+  right: 30px;
+}
+
+.mandate > * {
+  margin-top: 15px;
+}
+
+.modalTitle {
+  font-size: 30px;
+  font-weight: 600;
+  padding: 10px 0;
+}
+
+.fontS20 {
+  font-size: 20px;
+}
+
+.fontS22 {
+  font-size: 22px;
+}
+
+.fontS24 {
+  font-size: 24px;
+}
+
+.fontS26 {
+  font-size: 26px;
+}
+
+.fontS28 {
+  font-size: 28px;
+}
+
+.fontS30 {
+  font-size: 30px;
+}

+ 589 - 0
src/pages/TaskManage/components/MandateDetail.js

@@ -0,0 +1,589 @@
+import { queryMandate } from '@/services/SmartOps';
+import { UnityAction } from '@/utils/utils';
+import { connect, useRequest } from '@umijs/max';
+import {
+  Button,
+  Checkbox,
+  Col,
+  ConfigProvider,
+  DatePicker,
+  Divider,
+  Form,
+  Input,
+  Modal,
+  Row,
+  Select,
+  Table,
+  message,
+} from 'antd';
+import dayjs from 'dayjs';
+import { useEffect, useState } from 'react';
+import {
+  MandateClass,
+  MandateStatus,
+  MandateType,
+  OrderStatus,
+  OrderType,
+  ignoreReason,
+} from '../constent';
+import styles from './MandateDetail.less';
+
+import zhCN from 'antd/es/locale/zh_CN';
+
+const MandateDetail = (props) => {
+  const {
+    projectID,
+    mandateID,
+    userList,
+    ignoreTask,
+    dispatchTask,
+    autoHandleTask,
+    dispatch,
+  } = props;
+
+  useEffect(() => {
+    if (userList.length === 0) {
+      dispatch({
+        type: 'taskUser/fetchUserList',
+        payload: { project_id: projectID },
+      });
+    }
+  }, []);
+
+  const [mandateDetail, setMandateDetail] = useState();
+  const [mandateChild, setMandateChild] = useState([]);
+  const [handledWorkOrder, setHandledWorkOrder] = useState([]);
+
+  const [ignoreModalOpen, setIgnoreModalOpen] = useState(false);
+  const [autoHandleModalOpen, setAutoHandleModalOpen] = useState(false);
+  const [mandateSelectModalOpen, setMandateSelectModalOpen] = useState(false);
+  const [selectedTask, setSelectedTask] = useState([]);
+  const [dispatchModalOpen, setDispatchModalOpen] = useState(false);
+
+  const columns = [
+    {
+      title: '参数',
+      dataIndex: 'Title',
+    },
+    {
+      title: '调整内容',
+      dataIndex: 'Content',
+    },
+  ];
+
+  const { run: getMandateInfo } = useRequest(queryMandate, {
+    manual: true,
+    formatResult: (result) => {
+      if (result?.data) {
+        const tempMandate = {
+          ...result.data,
+          Status: MandateStatus.find(
+            (item) => item.value === result.data.Status,
+          ),
+          MandateClass: MandateClass.find(
+            (item) => item.value === result.data.MandateClass,
+          ),
+          MandateType: MandateType.find(
+            (item) => item.value === result.data.MandateType,
+          ),
+          ResponsiblePeople: userList.find(
+            (item) => item.ID === result.data.ResponsiblePeople,
+          ),
+          CreateTime: dayjs(result.data.CreateTime).format('YYYY-MM-DD HH:mm'),
+        };
+        const workOrder = result.data.Records.map((item) => {
+          return {
+            ...item,
+            CreateTime: dayjs(item.CreateTime).format('YYYY-MM-DD HH:mm'),
+            Status: OrderStatus.find((status) => status.value === item.Status),
+            RecordType: OrderType.find(
+              (type) => type.value === item.RecordType,
+            ),
+            Responsible: userList.find((user) => user.ID === item.Responsible),
+          };
+        });
+        setMandateDetail(tempMandate);
+        setMandateChild(tempMandate.MandateChild);
+        setHandledWorkOrder(workOrder);
+      }
+    },
+  });
+
+  // 打开指定弹窗
+  const openSpecifiedModal = (type) => {
+    switch (type) {
+      case 'ignore':
+        setIgnoreModalOpen(true);
+        break;
+      case 'manual':
+        UnityAction.sendMsg('menuItem', '工艺监控');
+        break;
+      case 'auto':
+        setAutoHandleModalOpen(true);
+        break;
+      case 'dispatch':
+        setMandateSelectModalOpen(true);
+        break;
+    }
+  };
+
+  // 忽略
+  const onIgnoreConfirm = async (reason) => {
+    const result = await ignoreTask(mandateID, reason);
+    if (result) {
+      setIgnoreModalOpen(false);
+      getMandateInfo({ mandate_id: mandateID });
+    }
+  };
+
+  const onAutoHandleConfirm = async (pw) => {
+    const result = await autoHandleTask(pw, mandateDetail);
+    if (result) {
+      setAutoHandleModalOpen(false);
+      getMandateInfo({ mandate_id: mandateID });
+    }
+  };
+
+  const onManualHandleConfirm = () => {
+    console.log('manual handle Confirm');
+  };
+
+  const onMandateSelected = (records) => {
+    // 打开派单Form弹窗将选中的任务进行派遣
+    if (records?.length === 0) {
+      message.warning('请先选择要派遣的任务');
+      return;
+    }
+    setSelectedTask(records);
+    setDispatchModalOpen(true);
+  };
+
+  const onDispatchConfirm = async (value) => {
+    const params = {
+      ...value,
+      m_id: Number(mandateID),
+      mc_id: selectedTask.join(),
+      plan_end_time: dayjs(value.plan_end_time).format('YYYY-MM-DD HH:mm:ss'),
+    };
+    if (params.type === 5) {
+      if (params.mc_id.split(',').length > 1) {
+        message.warning('加药工单不可批量派遣');
+        return;
+      }
+      params.note = `${
+        mandateChild
+          .find((mandate) => mandate.Id === Number(params.mc_id))
+          ?.Title?.split(':')[1] + ',请及时加药'
+      }`;
+    } else {
+      params.note = mandateDetail.Summary;
+    }
+    const result = await dispatchTask(params);
+    if (result) {
+      setDispatchModalOpen(false);
+      getMandateInfo({ mandate_id: mandateID });
+    }
+  };
+
+  const openWorkOrderModal = (record) => {
+    UnityAction.sendMsg(
+      'OpenWorkOrderModal',
+      `order_id=${record.Id}&order_type=${record.RecordType.value}`,
+    );
+  };
+
+  useEffect(() => {
+    getMandateInfo({ mandate_id: mandateID });
+  }, []);
+
+  return (
+    <div>
+      <div className={styles.mandate}>
+        <Row>
+          <Col className={styles.fontS28}>任务内容:</Col>
+          <Col className={styles.fontS28} span={21}>
+            <Table
+              columns={columns}
+              dataSource={mandateChild}
+              pagination={false}
+            />
+          </Col>
+        </Row>
+        <Row>
+          <Col className={styles.fontS28} span={14}>
+            任务时间:{mandateDetail?.CreateTime || '-'}
+          </Col>
+          <Col className={styles.fontS28}>
+            任务类别:{mandateDetail?.MandateClass?.label || '-'}
+          </Col>
+        </Row>
+        <Row>
+          <Col className={styles.fontS28} span={14}>
+            任务状态:{' '}
+            <span style={{ color: ' #5697e4' }}>
+              {mandateDetail?.Status.label}
+            </span>
+          </Col>
+          <Col className={styles.fontS28}>
+            任务负责人:{mandateDetail?.ResponsiblePeople?.CName || '-'}
+          </Col>
+        </Row>
+      </div>
+      {handledWorkOrder?.length > 0 && <Divider />}
+      {handledWorkOrder?.map((item) => {
+        return (
+          <div key={item.Id} className={styles.relatedOrder}>
+            <div className={styles.leftInfo}>
+              <Row>
+                <Col span={12} className={styles.fontS28}>
+                  工单类型:{item?.RecordType?.label}
+                </Col>
+                <Col className={styles.fontS28}>时间:{item?.CreateTime}</Col>
+              </Row>
+              <Row>
+                <Col span={12} className={styles.fontS28}>
+                  工单状态:
+                  <span style={{ color: ' #5697e4' }}>
+                    {item?.Status?.label}
+                  </span>
+                </Col>
+                <Col className={styles.fontS28}>
+                  工单负责人:{item?.Responsible?.CName}
+                </Col>
+              </Row>
+            </div>
+            <Divider type="vertical" style={{ height: '40px' }} />
+            <div
+              className={styles.rightButton}
+              onClick={() => openWorkOrderModal(item)}
+            >
+              查看工单
+            </div>
+          </div>
+        );
+      })}
+      {mandateDetail?.Status?.value === 0 && <Divider />}
+      {mandateDetail?.Status?.value === 0 && (
+        <div className={styles.footerConstainer}>
+          <Button
+            className={styles.footerBtn}
+            shape="round"
+            onClick={() => {
+              openSpecifiedModal('ignore');
+            }}
+          >
+            忽略
+          </Button>
+          {(mandateDetail?.MandateClass?.value === 1 ||
+            mandateDetail?.MandateClass?.value === 2) &&
+            mandateDetail?.Status?.value === 0 && (
+              <>
+                <Button
+                  className={styles.footerBtn}
+                  shape="round"
+                  onClick={() => {
+                    openSpecifiedModal('manual');
+                  }}
+                >
+                  手动处理
+                </Button>
+                <Button
+                  className={styles.footerBtn}
+                  shape="round"
+                  onClick={() => {
+                    openSpecifiedModal('auto');
+                  }}
+                >
+                  自动处理
+                </Button>
+              </>
+            )}
+          <Button
+            className={styles.footerBtn}
+            shape="round"
+            onClick={() => {
+              openSpecifiedModal('dispatch');
+            }}
+          >
+            派单
+          </Button>
+        </div>
+      )}
+      {/* 弹窗 */}
+      <ConfigProvider locale={zhCN}>
+        <IgnoreTaskModal
+          open={ignoreModalOpen}
+          onCancel={() => setIgnoreModalOpen(false)}
+          onOk={onIgnoreConfirm}
+        />
+        <AutoHandleModal
+          open={autoHandleModalOpen}
+          onCancel={() => setAutoHandleModalOpen(false)}
+          onOk={onAutoHandleConfirm}
+        />
+        <MandateSelectModal
+          open={mandateSelectModalOpen}
+          onCancel={() => setMandateSelectModalOpen(false)}
+          selectedTask={selectedTask}
+          setSelectedTask={setSelectedTask}
+          onOk={onMandateSelected}
+          list={mandateChild}
+        />
+        <DispatchTaskModal
+          open={dispatchModalOpen}
+          userList={userList}
+          onCancel={() => {
+            setDispatchModalOpen(false);
+          }}
+          onOK={onDispatchConfirm}
+        />
+      </ConfigProvider>
+    </div>
+  );
+};
+
+export default connect(({ taskUser }) => {
+  return {
+    userList: taskUser.userList,
+  };
+})(MandateDetail);
+
+const IgnoreTaskModal = (params) => {
+  const { open, onCancel, onOk } = params;
+
+  const [ignoreReasonText, setIgnoreReasonText] = useState('');
+  const [selectedReason, setSelectedReason] = useState({});
+  const [showInput, setShowInput] = useState(false);
+
+  const onReasonChange = (reason, option) => {
+    if (reason !== 4) {
+      setSelectedReason(option);
+      setShowInput(false);
+    } else {
+      setShowInput(true);
+    }
+  };
+
+  const onReasonTextChange = (e) => {
+    setIgnoreReasonText(e.target.value);
+  };
+
+  const confirmIgnore = () => {
+    if (showInput) {
+      if (!ignoreReasonText.length) {
+        message.warning('请输入忽略理由');
+      } else {
+        onOk(ignoreReasonText);
+      }
+    } else {
+      if (selectedReason?.label) {
+        onOk(selectedReason.label);
+      } else {
+        message.warning('请选择忽略理由');
+      }
+    }
+  };
+
+  return (
+    <Modal
+      title="忽略"
+      open={open}
+      onCancel={onCancel}
+      onOk={confirmIgnore}
+      destroyOnClose
+    >
+      <div style={{ padding: '15px' }}>
+        <Form layout="vertical">
+          <Form.Item label="忽略理由:">
+            <Select
+              className={styles.fontS28}
+              options={ignoreReason}
+              onChange={onReasonChange}
+              allowClear
+            />
+          </Form.Item>
+          {showInput && (
+            <Form.Item label="输入理由:">
+              <Input placeholder="请输入理由" onChange={onReasonTextChange} />
+            </Form.Item>
+          )}
+        </Form>
+      </div>
+    </Modal>
+  );
+};
+
+const AutoHandleModal = (props) => {
+  const { open, onCancel, onOk } = props;
+
+  const [automation, setAutomation] = useState();
+
+  const confirmAutoHandle = () => {
+    if (automation.length) {
+      onOk(automation);
+    } else {
+      message.warning('请输入口令');
+    }
+  };
+
+  return (
+    <Modal
+      title="自动处理"
+      open={open}
+      onCancel={onCancel}
+      onOk={confirmAutoHandle}
+      destroyOnClose
+    >
+      <div style={{ padding: '15px' }}>
+        <Form layout="vertical">
+          <Form.Item label="口令:">
+            {
+              <Input
+                autoFocus
+                style={{ width: '100%' }}
+                placeholder="请输入口令"
+                onChange={(e) => {
+                  setAutomation(e.target.value);
+                }}
+              />
+            }
+          </Form.Item>
+        </Form>
+      </div>
+    </Modal>
+  );
+};
+
+const MandateSelectModal = (props) => {
+  const { open, onCancel, list, onOk, selectedTask, setSelectedTask } = props;
+
+  const [checkOptions, setCheckOptions] = useState([]);
+
+  useEffect(() => {
+    setCheckOptions(
+      list.map((mandate, index) => {
+        return {
+          label: (
+            <Row className={styles.taskCheckItem}>
+              <span
+                style={{
+                  textDecoration: `${
+                    mandate.Status === 0 ? '' : 'line-through'
+                  }`,
+                }}
+              >
+                {`${index + 1}. ${mandate.Title}为${mandate.Content}`}
+              </span>
+            </Row>
+          ),
+          value: mandate.Id,
+          disabled: mandate.Status !== 0,
+        };
+      }),
+    );
+  }, [list]);
+
+  const onDispatchClick = () => {
+    onOk(selectedTask);
+  };
+
+  const handleCheckChange = (checkedValue) => {
+    setSelectedTask(checkedValue);
+  };
+
+  return (
+    <Modal
+      title={<span className={styles.fontS28}>选择任务</span>}
+      open={open}
+      onCancel={onCancel}
+      width={'95%'}
+      destroyOnClose
+      footer={[
+        <Button key="back" onClick={onCancel}>
+          取消
+        </Button>,
+        <Button key="dispatch" type="primary" onClick={onDispatchClick}>
+          派单
+        </Button>,
+      ]}
+    >
+      <Checkbox.Group
+        className={styles.taskCheckBox}
+        options={checkOptions}
+        onChange={handleCheckChange}
+      />
+    </Modal>
+  );
+};
+
+const DispatchTaskModal = (props) => {
+  const { open, onCancel, onOK, userList } = props;
+
+  const [form] = Form.useForm();
+
+  useEffect(() => {
+    if (!open) {
+      form.resetFields();
+    }
+  }, [open]);
+
+  const handleDispatchConfirm = async () => {
+    const value = await form.validateFields().catch((err) =>
+      err.errorFields.forEach((item) => {
+        message.error(item.errors);
+      }),
+    );
+    if (!value) {
+      return;
+    }
+    onOK(value);
+  };
+
+  return (
+    <Modal
+      title={<span className={styles.fontS28}>派遣任务</span>}
+      onCancel={onCancel}
+      open={open}
+      destroyOnClose
+      style={{ padding: '20px 0' }}
+      onOk={handleDispatchConfirm}
+    >
+      <Form
+        form={form}
+        layout="horizontal"
+        labelCol={{ span: 6 }}
+        wrapperCol={{ span: 18 }}
+      >
+        <Form.Item
+          label="工单类型"
+          name="type"
+          rules={[{ required: true, message: '请选择工单类型' }]}
+        >
+          <Select options={OrderType} placeholder="请选择工单类型" />
+        </Form.Item>
+        <Form.Item
+          label="操作人"
+          name="operator_id"
+          rules={[{ required: true, message: '请选择操作人' }]}
+        >
+          <Select
+            options={userList.map((item) => {
+              return {
+                label: item.CName,
+                value: item.ID,
+              };
+            })}
+            placeholder="请选择操作人"
+          />
+        </Form.Item>
+        <Form.Item
+          label="计划完成时间"
+          name="plan_end_time"
+          rules={[{ required: true, message: '请选择完成时间' }]}
+        >
+          <DatePicker style={{ width: '100%' }} placeholder="请选择完成时间" />
+        </Form.Item>
+      </Form>
+    </Modal>
+  );
+};

+ 94 - 0
src/pages/TaskManage/components/MandateDetail.less

@@ -0,0 +1,94 @@
+.modalContainer {
+  padding: 0 30px;
+}
+
+.closeModalBtn {
+  position: absolute;
+  top: 15px;
+  right: 20px;
+}
+
+.mandate > * {
+  margin-top: 15px;
+}
+
+.modalTitle {
+  font-size: 28px;
+  font-weight: 600;
+  padding: 10px 0;
+}
+
+.footerConstainer {
+  display: flex;
+  flex-direction: row;
+  justify-content: flex-end;
+  .footerBtn {
+    height: 50px;
+    width: 125px;
+    font-size: 26px;
+    border: 0;
+    color: #5697e4;
+    background-color: #e5effa;
+  }
+}
+
+.footerConstainer > * {
+  margin-left: 10px;
+}
+
+.relatedOrder {
+  margin-bottom: 20px;
+  background-color: #e5effa;
+  border-radius: 8px;
+  display: flex;
+  align-items: center;
+  padding: 15px;
+  .leftInfo {
+    width: 80%;
+  }
+  .leftInfo > * {
+    padding: 5px;
+  }
+  .rightButton {
+    flex: auto;
+    color: #5697e4;
+    font-size: 28px;
+    text-align: center;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+  }
+}
+
+.taskCheckBox {
+  padding: 10px;
+}
+
+.taskCheckItem {
+  font-size: 20px;
+  padding: 10px;
+}
+
+.fontS20 {
+  font-size: 20px;
+}
+
+.fontS22 {
+  font-size: 22px;
+}
+
+.fontS24 {
+  font-size: 24px;
+}
+
+.fontS26 {
+  font-size: 26px;
+}
+
+.fontS28 {
+  font-size: 28px;
+}
+
+.fontS30 {
+  font-size: 30px;
+}

برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است