瀏覽代碼

feat(merge): merge xyh

Next xyh
Lind 3 年之前
父節點
當前提交
4f2afc51a6
共有 31 個文件被更改,包括 584 次插入335 次删除
  1. 13 13
      src/components/BaseCrud/index.tsx
  2. 5 5
      src/components/Metadata/ArrayParam/index.tsx
  3. 4 4
      src/components/PermissionButton/index.tsx
  4. 4 0
      src/components/Player/ScreenPlayer.tsx
  5. 14 7
      src/components/Player/index.tsx
  6. 4 4
      src/components/ProTableCard/CardItems/AccessConfig/index.tsx
  7. 14 0
      src/components/ProTableCard/index.less
  8. 45 25
      src/components/ProTableCard/index.tsx
  9. 4 4
      src/hooks/permission/index.ts
  10. 8 8
      src/pages/device/Category/Save/index.tsx
  11. 12 7
      src/pages/device/Instance/Detail/Config/index.tsx
  12. 8 8
      src/pages/device/Instance/Detail/Diagnose/Status/index.tsx
  13. 4 4
      src/pages/device/Instance/Detail/Diagnose/index.tsx
  14. 8 8
      src/pages/device/Instance/Detail/Info/index.tsx
  15. 12 12
      src/pages/device/Instance/Detail/index.tsx
  16. 7 7
      src/pages/device/Instance/Save/index.tsx
  17. 16 8
      src/pages/device/Instance/index.tsx
  18. 35 3
      src/pages/device/Product/Detail/BaseInfo/index.tsx
  19. 10 1
      src/pages/device/Product/Detail/index.tsx
  20. 14 14
      src/pages/device/components/Metadata/Base/index.tsx
  21. 8 8
      src/pages/device/components/Metadata/index.tsx
  22. 44 35
      src/pages/media/Device/Playback/index.tsx
  23. 56 37
      src/pages/media/Device/Playback/timeLine.tsx
  24. 1 1
      src/pages/media/Device/Save/ProviderSelect.tsx
  25. 1 1
      src/pages/media/Device/Save/SaveProduct.tsx
  26. 1 0
      src/pages/rule-engine/Scene/Save/index.tsx
  27. 177 92
      src/pages/rule-engine/Scene/index.tsx
  28. 9 0
      src/pages/rule-engine/Scene/service.ts
  29. 1 0
      src/pages/rule-engine/Scene/typings.d.ts
  30. 13 9
      src/pages/system/Menu/Detail/buttons.tsx
  31. 32 10
      src/pages/system/Menu/Detail/edit.tsx

+ 13 - 13
src/components/BaseCrud/index.tsx

@@ -1,22 +1,22 @@
-import {useIntl} from '@@/plugin-locale/localeExports';
-import {Button, Tooltip} from 'antd';
-import type {ActionType, ProColumns, RequestData} from '@jetlinks/pro-table';
+import { useIntl } from '@@/plugin-locale/localeExports';
+import { Button, Tooltip } from 'antd';
+import type { ActionType, ProColumns, RequestData } from '@jetlinks/pro-table';
 import ProTable from '@jetlinks/pro-table';
 
-import {PlusOutlined} from '@ant-design/icons';
+import { PlusOutlined } from '@ant-design/icons';
 import type BaseService from '@/utils/BaseService';
 import * as React from 'react';
-import {useRef, useState} from 'react';
+import { useRef, useState } from 'react';
 import Save from '@/components/BaseCrud/save';
-import type {ISchema} from '@formily/json-schema';
-import {CurdModel} from '@/components/BaseCrud/model';
-import type {ISchemaFieldProps} from '@formily/react/lib/types';
-import type {ModalProps} from 'antd/lib/modal/Modal';
-import type {TablePaginationConfig} from 'antd/lib/table/interface';
-import type {Form} from '@formily/core';
+import type { ISchema } from '@formily/json-schema';
+import { CurdModel } from '@/components/BaseCrud/model';
+import type { ISchemaFieldProps } from '@formily/react/lib/types';
+import type { ModalProps } from 'antd/lib/modal/Modal';
+import type { TablePaginationConfig } from 'antd/lib/table/interface';
+import type { Form } from '@formily/core';
 import SearchComponent from '@/components/SearchComponent';
-import type {ProFormInstance} from '@ant-design/pro-form';
-import type {SearchConfig} from '@ant-design/pro-form/lib/components/Submitter';
+import type { ProFormInstance } from '@ant-design/pro-form';
+import type { SearchConfig } from '@ant-design/pro-form/lib/components/Submitter';
 
 export type Option = {
   model: 'edit' | 'preview' | 'add';

+ 5 - 5
src/components/Metadata/ArrayParam/index.tsx

@@ -1,9 +1,9 @@
-import {createSchemaField} from '@formily/react';
-import {Editable, FormItem, FormLayout, Input, NumberPicker, Select} from '@formily/antd';
-import type {ISchema} from '@formily/json-schema';
+import { createSchemaField } from '@formily/react';
+import { Editable, FormItem, FormLayout, Input, NumberPicker, Select } from '@formily/antd';
+import type { ISchema } from '@formily/json-schema';
 import './index.less';
-import {DataTypeList, DateTypeList, FileTypeList} from '@/pages/device/data';
-import {Store} from 'jetlinks-store';
+import { DataTypeList, DateTypeList, FileTypeList } from '@/pages/device/data';
+import { Store } from 'jetlinks-store';
 import JsonParam from '@/components/Metadata/JsonParam';
 import EnumParam from '@/components/Metadata/EnumParam';
 import BooleanEnum from '@/components/Metadata/BooleanParam';

+ 4 - 4
src/components/PermissionButton/index.tsx

@@ -1,8 +1,8 @@
-import type {ButtonProps, PopconfirmProps, TooltipProps} from 'antd';
-import {Button, Popconfirm, Tooltip} from 'antd';
+import type { ButtonProps, PopconfirmProps, TooltipProps } from 'antd';
+import { Button, Popconfirm, Tooltip } from 'antd';
 import usePermissions from '@/hooks/permission';
-import {useCallback} from 'react';
-import {useIntl} from '@@/plugin-locale/localeExports';
+import { useCallback } from 'react';
+import { useIntl } from '@@/plugin-locale/localeExports';
 
 interface PermissionButtonProps extends ButtonProps {
   tooltip?: TooltipProps;

+ 4 - 0
src/components/Player/ScreenPlayer.tsx

@@ -70,6 +70,7 @@ export default forwardRef((props: ScreenProps, ref) => {
   const [playerActive, setPlayerActive] = useState(0);
   const [historyList, setHistoryList] = useState<any>([]);
   const [visible, setVisible] = useState(false);
+  const [loading, setLoading] = useState(false);
 
   const fullscreenRef = useRef(null);
   const [isFullscreen, { setFull }] = useFullscreen(fullscreenRef);
@@ -189,7 +190,9 @@ export default forwardRef((props: ScreenProps, ref) => {
         players: players.map((item) => ({ deviceId: item.id, channelId: item.channelId })),
       }),
     };
+    setLoading(true);
     const resp = await service.history.save(DEFAULT_SAVE_CODE, param);
+    setLoading(false);
     if (resp.status === 200) {
       setVisible(false);
       getHistory();
@@ -347,6 +350,7 @@ export default forwardRef((props: ScreenProps, ref) => {
                       <Button
                         type={'primary'}
                         onClick={saveHistory}
+                        loading={loading}
                         style={{ width: '100%', marginTop: 16 }}
                       >
                         保存

+ 14 - 7
src/components/Player/index.tsx

@@ -1,4 +1,4 @@
-import { forwardRef, useEffect, useImperativeHandle, useRef } from 'react';
+import { useEffect, useRef, forwardRef, useImperativeHandle, useState } from 'react';
 import { isFunction } from 'lodash';
 
 export type PlayerProps = {
@@ -12,17 +12,18 @@ export type PlayerProps = {
   updateTime?: number;
   key?: string | number;
   loading?: boolean;
-  onDestroy?: () => void;
+  onDestroy?: (e?: any) => void;
   onMessage?: (msg: any) => void;
   onError?: (err: any) => void;
   onTimeUpdate?: (time: any) => void;
-  onPause?: () => void;
-  onPlay?: () => void;
+  onPause?: (e?: any) => void;
+  onPlay?: (e?: any) => void;
   protocol?: 'mp4' | 'flv' | 'hls';
   onFullscreen?: () => void;
   onSnapOutside?: (base64: any) => void;
   onSnapInside?: (base64: any) => void;
   onCustomButtons?: (name: any) => void;
+  onEnded?: (e?: any) => void;
   onClick?: () => void;
 };
 
@@ -36,9 +37,11 @@ const EventsEnum = {
   snapOutside: 'onSnapOutside',
   snapInside: 'onSnapInside',
   customButtons: 'onCustomButtons',
+  ended: 'onEnded',
 };
 const LivePlayer = forwardRef((props: PlayerProps, ref) => {
   const player = useRef<HTMLVideoElement>(null);
+  const [url, setUrl] = useState(props.url);
 
   useEffect(() => {
     return () => {
@@ -49,14 +52,18 @@ const LivePlayer = forwardRef((props: PlayerProps, ref) => {
     };
   }, []);
 
+  useEffect(() => {
+    setUrl(props.url);
+  }, [props.url]);
+
   /**
    * 事件初始化
    */
   const EventInit = () => {
     for (const key in EventsEnum) {
-      player.current?.addEventListener(key, () => {
+      player.current?.addEventListener(key, (e) => {
         if (EventsEnum[key] in props) {
-          props[EventsEnum[key]]();
+          props[EventsEnum[key]](e);
         }
       });
     }
@@ -84,7 +91,7 @@ const LivePlayer = forwardRef((props: PlayerProps, ref) => {
       hide-big-play-button={true}
       poster={props.poster || ''}
       timeout={props.timeout || 20}
-      video-url={props.url || ''}
+      video-url={url || ''}
     />
   );
 });

+ 4 - 4
src/components/ProTableCard/CardItems/AccessConfig/index.tsx

@@ -1,9 +1,9 @@
 import React from 'react';
-import {StatusColorEnum} from '@/components/BadgeStatus';
-import {TableCard} from '@/components';
+import { StatusColorEnum } from '@/components/BadgeStatus';
+import { TableCard } from '@/components';
 import '@/style/common.less';
-import {Badge, Tooltip} from 'antd';
-import type {AccessItem} from '@/pages/link/AccessConfig/typings';
+import { Badge, Tooltip } from 'antd';
+import type { AccessItem } from '@/pages/link/AccessConfig/typings';
 import './index.less';
 import classNames from 'classnames';
 

+ 14 - 0
src/components/ProTableCard/index.less

@@ -88,6 +88,20 @@
       display: none;
     }
   }
+
+  .mask-loading {
+    position: absolute;
+    top: 0;
+    left: 0;
+    z-index: 99;
+    display: none;
+    width: 100%;
+    height: 100%;
+
+    &.show {
+      display: block;
+    }
+  }
 }
 
 .iot-card {

+ 45 - 25
src/components/ProTableCard/index.tsx

@@ -1,11 +1,12 @@
-import type {ProTableProps} from '@jetlinks/pro-table';
+import type { ProTableProps } from '@jetlinks/pro-table';
 import ProTable from '@jetlinks/pro-table';
-import type {ParamsType} from '@ant-design/pro-provider';
-import React, {useEffect, useState} from 'react';
-import {isFunction} from 'lodash';
-import {Empty, Pagination, Space} from 'antd';
-import {AppstoreOutlined, BarsOutlined} from '@ant-design/icons';
+import type { ParamsType } from '@ant-design/pro-provider';
+import React, { useEffect, useState } from 'react';
+import { isFunction } from 'lodash';
+import { Empty, Pagination, Space } from 'antd';
+import { AppstoreOutlined, BarsOutlined } from '@ant-design/icons';
 import classNames from 'classnames';
+import LoadingComponent from '@ant-design/pro-layout/es/PageLoading';
 import './index.less';
 
 enum ModelEnum {
@@ -36,12 +37,15 @@ const ProTableCard = <
   const [pageIndex, setPageIndex] = useState(0);
   const [pageSize, setPageSize] = useState(Default_Size * 2); // 每页条数
   const [column, setColumn] = useState(props.gridColumn || 4);
+  const [loading, setLoading] = useState(false);
+  const [dataLength, setDataLength] = useState<number>(0);
 
   /**
    * 处理 Card
    * @param dataSource
    */
   const handleCard = (dataSource: readonly T[] | undefined): JSX.Element => {
+    setDataLength(dataSource ? dataSource.length : 0);
     return (
       <>
         {dataSource && dataSource.length ? (
@@ -82,6 +86,11 @@ const ProTableCard = <
 
   const pageSizeOptions = [Default_Size * 2, Default_Size * 4, Default_Size * 8, Default_Size * 16];
 
+  useEffect(() => {
+    setCurrent(1);
+    setPageIndex(0);
+  }, [props.params]);
+
   return (
     <div className={'pro-table-card'}>
       <ProTable<T, U, ValueType>
@@ -112,6 +121,10 @@ const ProTableCard = <
           }
           return {};
         }}
+        onLoadingChange={(l) => {
+          console.log(l);
+          setLoading(!!l);
+        }}
         pagination={{
           onChange: (page, size) => {
             setCurrent(page);
@@ -163,25 +176,32 @@ const ProTableCard = <
         }
       />
       {model === ModelEnum.CARD && (
-        <Pagination
-          showSizeChanger
-          size="small"
-          className={'pro-table-card-pagination'}
-          total={total}
-          current={current}
-          onChange={(page, size) => {
-            setCurrent(page);
-            setPageIndex(page - 1);
-            setPageSize(size);
-          }}
-          pageSizeOptions={pageSizeOptions}
-          pageSize={pageSize}
-          showTotal={(num) => {
-            const minSize = pageIndex * pageSize + 1;
-            const MaxSize = (pageIndex + 1) * pageSize;
-            return `第 ${minSize} - ${MaxSize > num ? num : MaxSize} 条/总共 ${num} 条`;
-          }}
-        />
+        <>
+          <div className={classNames('mask-loading', { show: loading })}>
+            <LoadingComponent />
+          </div>
+          {!!dataLength && (
+            <Pagination
+              showSizeChanger
+              size="small"
+              className={'pro-table-card-pagination'}
+              total={total}
+              current={current}
+              onChange={(page, size) => {
+                setCurrent(page);
+                setPageIndex(page - 1);
+                setPageSize(size);
+              }}
+              pageSizeOptions={pageSizeOptions}
+              pageSize={pageSize}
+              showTotal={(num) => {
+                const minSize = pageIndex * pageSize + 1;
+                const MaxSize = (pageIndex + 1) * pageSize;
+                return `第 ${minSize} - ${MaxSize > num ? num : MaxSize} 条/总共 ${num} 条`;
+              }}
+            />
+          )}
+        </>
       )}
     </div>
   );

+ 4 - 4
src/hooks/permission/index.ts

@@ -1,7 +1,7 @@
-import {useEffect, useState} from 'react';
-import type {BUTTON_PERMISSION, MENUS_CODE_TYPE} from '@/utils/menu/router';
-import {BUTTON_PERMISSION_ENUM} from '@/utils/menu/router';
-import {MENUS_BUTTONS_CACHE} from '@/utils/menu';
+import { useEffect, useState } from 'react';
+import type { BUTTON_PERMISSION, MENUS_CODE_TYPE } from '@/utils/menu/router';
+import { BUTTON_PERMISSION_ENUM } from '@/utils/menu/router';
+import { MENUS_BUTTONS_CACHE } from '@/utils/menu';
 
 export type permissionKeyType = keyof typeof BUTTON_PERMISSION_ENUM;
 export type permissionType = Record<permissionKeyType, boolean>;

+ 8 - 8
src/pages/device/Category/Save/index.tsx

@@ -13,16 +13,16 @@ import {
   Upload,
 } from '@formily/antd';
 import React from 'react';
-import {createForm} from '@formily/core';
-import {createSchemaField} from '@formily/react';
+import { createForm } from '@formily/core';
+import { createSchemaField } from '@formily/react';
 import FUpload from '@/components/Upload';
 import * as ICONS from '@ant-design/icons';
-import {message, Modal} from 'antd';
-import {useIntl} from '@@/plugin-locale/localeExports';
-import type {ISchema} from '@formily/json-schema';
-import type {CategoryItem} from '@/pages/visualization/Category/typings';
-import {service, state} from '@/pages/device/Category';
-import type {Response} from '@/utils/typings';
+import { message, Modal } from 'antd';
+import { useIntl } from '@@/plugin-locale/localeExports';
+import type { ISchema } from '@formily/json-schema';
+import type { CategoryItem } from '@/pages/visualization/Category/typings';
+import { service, state } from '@/pages/device/Category';
+import type { Response } from '@/utils/typings';
 
 interface Props {
   visible: boolean;

+ 12 - 7
src/pages/device/Instance/Detail/Config/index.tsx

@@ -1,11 +1,16 @@
-import {Descriptions, message, Space, Tooltip} from 'antd';
-import {InstanceModel, service} from '@/pages/device/Instance';
-import {useEffect, useState} from 'react';
-import type {ConfigMetadata} from '@/pages/device/Product/typings';
-import {history, useParams} from 'umi';
-import {CheckOutlined, EditOutlined, QuestionCircleOutlined, UndoOutlined,} from '@ant-design/icons';
+import { Descriptions, message, Space, Tooltip } from 'antd';
+import { InstanceModel, service } from '@/pages/device/Instance';
+import { useEffect, useState } from 'react';
+import type { ConfigMetadata } from '@/pages/device/Product/typings';
+import { history, useParams } from 'umi';
+import {
+  CheckOutlined,
+  EditOutlined,
+  QuestionCircleOutlined,
+  UndoOutlined,
+} from '@ant-design/icons';
 import Edit from './Edit';
-import {PermissionButton} from '@/components';
+import { PermissionButton } from '@/components';
 
 const Config = () => {
   const params = useParams<{ id: string }>();

+ 8 - 8
src/pages/device/Instance/Detail/Diagnose/Status/index.tsx

@@ -1,13 +1,13 @@
 import TitleComponent from '@/components/TitleComponent';
-import {Badge, Button, Col, message, Popconfirm, Row} from 'antd';
-import {useEffect, useState} from 'react';
+import { Badge, Button, Col, message, Popconfirm, Row } from 'antd';
+import { useEffect, useState } from 'react';
 import styles from './index.less';
-import {InstanceModel, service} from '@/pages/device/Instance';
-import {getMenuPathByParams, MENUS_CODE} from '@/utils/menu';
-import type {ProductItem} from '@/pages/device/Product/typings';
-import {Store} from 'jetlinks-store';
-import {observer} from '@formily/reactive-react';
-import {DiagnoseStatusModel} from './model';
+import { InstanceModel, service } from '@/pages/device/Instance';
+import { getMenuPathByParams, MENUS_CODE } from '@/utils/menu';
+import type { ProductItem } from '@/pages/device/Product/typings';
+import { Store } from 'jetlinks-store';
+import { observer } from '@formily/reactive-react';
+import { DiagnoseStatusModel } from './model';
 
 interface Props {
   onChange: (type: string) => void;

+ 4 - 4
src/pages/device/Instance/Detail/Diagnose/index.tsx

@@ -1,11 +1,11 @@
-import {Badge, Card, Col, Row} from 'antd';
-import type {ReactNode} from 'react';
-import {useEffect, useState} from 'react';
+import { Badge, Card, Col, Row } from 'antd';
+import type { ReactNode } from 'react';
+import { useEffect, useState } from 'react';
 import Message from './Message';
 import Status from './Status';
 import './index.less';
 import classNames from 'classnames';
-import {Store} from 'jetlinks-store';
+import { Store } from 'jetlinks-store';
 
 interface ListProps {
   key: string;

+ 8 - 8
src/pages/device/Instance/Detail/Info/index.tsx

@@ -1,15 +1,15 @@
-import {Card, Descriptions} from 'antd';
-import {InstanceModel} from '@/pages/device/Instance';
+import { Card, Descriptions } from 'antd';
+import { InstanceModel } from '@/pages/device/Instance';
 import moment from 'moment';
-import {observer} from '@formily/react';
-import {useIntl} from '@@/plugin-locale/localeExports';
+import { observer } from '@formily/react';
+import { useIntl } from '@@/plugin-locale/localeExports';
 import Config from '@/pages/device/Instance/Detail/Config';
 import Save from '../../Save';
-import {useState} from 'react';
-import type {DeviceInstance} from '../../typings';
-import {EditOutlined} from '@ant-design/icons';
+import { useState } from 'react';
+import type { DeviceInstance } from '../../typings';
+import { EditOutlined } from '@ant-design/icons';
 import Tags from '@/pages/device/Instance/Detail/Tags';
-import {PermissionButton} from '@/components';
+import { PermissionButton } from '@/components';
 
 const Info = observer(() => {
   const intl = useIntl();

+ 12 - 12
src/pages/device/Instance/Detail/index.tsx

@@ -1,10 +1,10 @@
-import {PageContainer} from '@ant-design/pro-layout';
-import {InstanceModel, service} from '@/pages/device/Instance';
-import {history, useParams} from 'umi';
-import {Badge, Card, Descriptions, Divider, message} from 'antd';
-import type {ReactNode} from 'react';
-import {useEffect, useState} from 'react';
-import {observer} from '@formily/react';
+import { PageContainer } from '@ant-design/pro-layout';
+import { InstanceModel, service } from '@/pages/device/Instance';
+import { history, useParams } from 'umi';
+import { Badge, Card, Descriptions, Divider, message } from 'antd';
+import type { ReactNode } from 'react';
+import { useEffect, useState } from 'react';
+import { observer } from '@formily/react';
 import Log from '@/pages/device/Instance/Detail/Log';
 // import Alarm from '@/pages/device/components/Alarm';
 import Info from '@/pages/device/Instance/Detail/Info';
@@ -13,15 +13,15 @@ import Running from '@/pages/device/Instance/Detail/Running';
 import ChildDevice from '@/pages/device/Instance/Detail/ChildDevice';
 import Diagnose from '@/pages/device/Instance/Detail/Diagnose';
 import MetadataMap from '@/pages/device/Instance/Detail/MetadataMap';
-import {useIntl} from '@@/plugin-locale/localeExports';
+import { useIntl } from '@@/plugin-locale/localeExports';
 import Metadata from '../../components/Metadata';
-import type {DeviceMetadata} from '@/pages/device/Product/typings';
+import type { DeviceMetadata } from '@/pages/device/Product/typings';
 import MetadataAction from '@/pages/device/components/Metadata/DataBaseAction';
-import {Store} from 'jetlinks-store';
+import { Store } from 'jetlinks-store';
 import SystemConst from '@/utils/const';
-import {getMenuPathByCode, getMenuPathByParams, MENUS_CODE} from '@/utils/menu';
+import { getMenuPathByCode, getMenuPathByParams, MENUS_CODE } from '@/utils/menu';
 import useSendWebsocketMessage from '@/hooks/websocket/useSendWebsocketMessage';
-import {PermissionButton} from '@/components';
+import { PermissionButton } from '@/components';
 
 export const deviceStatus = new Map();
 deviceStatus.set('online', <Badge status="success" text={'在线'} />);

+ 7 - 7
src/pages/device/Instance/Save/index.tsx

@@ -1,10 +1,10 @@
-import {Col, Form, Input, message, Modal, Row, Select} from 'antd';
-import {service} from '@/pages/device/Instance';
-import type {DeviceInstance} from '../typings';
-import {useEffect, useState} from 'react';
-import {useIntl} from '@@/plugin-locale/localeExports';
-import {UploadImage} from '@/components';
-import {debounce} from 'lodash';
+import { Col, Form, Input, message, Modal, Row, Select } from 'antd';
+import { service } from '@/pages/device/Instance';
+import type { DeviceInstance } from '../typings';
+import { useEffect, useState } from 'react';
+import { useIntl } from '@@/plugin-locale/localeExports';
+import { UploadImage } from '@/components';
+import { debounce } from 'lodash';
 
 interface Props {
   visible: boolean;

+ 16 - 8
src/pages/device/Instance/index.tsx

@@ -154,13 +154,17 @@ const Instance = () => {
       key={'delete'}
       style={{ padding: 0 }}
       isPermission={permission.delete}
+      tooltip={
+        record.state.value !== 'notActive'
+          ? { title: intl.formatMessage({ id: 'pages.device.instance.deleteTip' }) }
+          : undefined
+      }
+      disabled={record.state.value !== 'notActive'}
       popConfirm={{
         title: intl.formatMessage({
-          id:
-            record.state.value === 'notActive'
-              ? 'pages.data.option.remove.tips'
-              : 'pages.device.instance.deleteTip',
+          id: 'pages.data.option.remove.tips',
         }),
+        disabled: record.state.value !== 'notActive',
         onConfirm: async () => {
           if (record.state.value === 'notActive') {
             await service.remove(record.id);
@@ -621,13 +625,17 @@ const Instance = () => {
                 isPermission={permission.delete}
                 type={'link'}
                 style={{ padding: 0 }}
+                tooltip={
+                  record.state.value !== 'notActive'
+                    ? { title: intl.formatMessage({ id: 'pages.device.instance.deleteTip' }) }
+                    : undefined
+                }
+                disabled={record.state.value !== 'notActive'}
                 popConfirm={{
                   title: intl.formatMessage({
-                    id:
-                      record.state.value === 'notActive'
-                        ? 'pages.data.option.remove.tips'
-                        : 'pages.device.instance.deleteTip',
+                    id: 'pages.data.option.remove.tips',
                   }),
+                  disabled: record.state.value !== 'notActive',
                   onConfirm: async () => {
                     if (record.state.value === 'notActive') {
                       await service.remove(record.id);

+ 35 - 3
src/pages/device/Product/Detail/BaseInfo/index.tsx

@@ -1,5 +1,5 @@
 import { productModel, service } from '@/pages/device/Product';
-import { Descriptions } from 'antd';
+import { Button, Descriptions } from 'antd';
 import { useState } from 'react';
 import { useIntl } from '@@/plugin-locale/localeExports';
 import { EditOutlined } from '@ant-design/icons';
@@ -12,7 +12,11 @@ import { PermissionButton } from '@/components';
 //   password: 'Password',
 // };
 
-const BaseInfo = () => {
+interface BaseInfoProps {
+  onJump?: (type?: string) => void;
+}
+
+const BaseInfo = (props: BaseInfoProps) => {
   const intl = useIntl();
   // const [metadata, setMetadata] = useState<ConfigMetadata[]>([]);
   // const [state, setState] = useState<boolean>(false);
@@ -190,7 +194,35 @@ const BaseInfo = () => {
             defaultMessage: '接入方式',
           })}
         >
-          {productModel.current?.transportProtocol}
+          {permission.update ? (
+            productModel.current?.transportProtocol ? (
+              <Button
+                type={'link'}
+                style={{ padding: 0 }}
+                onClick={() => {
+                  if (props.onJump) {
+                    props.onJump('access');
+                  }
+                }}
+              >
+                {productModel.current?.transportProtocol}
+              </Button>
+            ) : (
+              <Button
+                type={'link'}
+                style={{ padding: 0 }}
+                onClick={() => {
+                  if (props.onJump) {
+                    props.onJump('access');
+                  }
+                }}
+              >
+                配置接入方式
+              </Button>
+            )
+          ) : (
+            productModel.current?.transportProtocol
+          )}
         </Descriptions.Item>
 
         <Descriptions.Item

+ 10 - 1
src/pages/device/Product/Detail/index.tsx

@@ -146,7 +146,15 @@ const ProductDetail = observer(() => {
         id: 'pages.device.productDetail.base',
         defaultMessage: '配置信息',
       }),
-      component: <BaseInfo />,
+      component: (
+        <BaseInfo
+          onJump={(type) => {
+            if (type) {
+              setMode(type);
+            }
+          }}
+        />
+      ),
     },
     {
       key: 'metadata',
@@ -254,6 +262,7 @@ const ProductDetail = observer(() => {
           type={'primary'}
           popConfirm={{
             title: '确定应用配置?',
+            disabled: productModel.current?.state === 0,
             onConfirm: () => {
               changeDeploy('deploy');
             },

+ 14 - 14
src/pages/device/components/Metadata/Base/index.tsx

@@ -1,24 +1,24 @@
-import type {ProColumns} from '@jetlinks/pro-table';
+import type { ProColumns } from '@jetlinks/pro-table';
 import ProTable from '@jetlinks/pro-table';
-import {useCallback, useEffect, useState} from 'react';
-import {useParams} from 'umi';
+import { useCallback, useEffect, useState } from 'react';
+import { useParams } from 'umi';
 import DB from '@/db';
-import type {MetadataItem, MetadataType} from '@/pages/device/Product/typings';
+import type { MetadataItem, MetadataType } from '@/pages/device/Product/typings';
 import MetadataMapping from './columns';
-import {message} from 'antd';
-import {DeleteOutlined, EditOutlined, ImportOutlined, PlusOutlined} from '@ant-design/icons';
+import { message } from 'antd';
+import { DeleteOutlined, EditOutlined, ImportOutlined, PlusOutlined } from '@ant-design/icons';
 import Edit from './Edit';
-import {observer} from '@formily/react';
+import { observer } from '@formily/react';
 import MetadataModel from './model';
-import {Store} from 'jetlinks-store';
+import { Store } from 'jetlinks-store';
 import SystemConst from '@/utils/const';
-import {useIntl} from '@@/plugin-locale/localeExports';
+import { useIntl } from '@@/plugin-locale/localeExports';
 import PropertyImport from '@/pages/device/Product/Detail/PropertyImport';
-import {productModel} from '@/pages/device/Product';
-import {InstanceModel} from '@/pages/device/Instance';
-import {asyncUpdateMedata, removeMetadata} from '../metadata';
-import type {permissionType} from '@/hooks/permission';
-import {PermissionButton} from '@/components';
+import { productModel } from '@/pages/device/Product';
+import { InstanceModel } from '@/pages/device/Instance';
+import { asyncUpdateMedata, removeMetadata } from '../metadata';
+import type { permissionType } from '@/hooks/permission';
+import { PermissionButton } from '@/components';
 
 interface Props {
   type: MetadataType;

+ 8 - 8
src/pages/device/components/Metadata/index.tsx

@@ -1,16 +1,16 @@
-import {observer} from '@formily/react';
-import {Space, Tabs} from 'antd';
+import { observer } from '@formily/react';
+import { Space, Tabs } from 'antd';
 import BaseMetadata from './Base';
-import {useIntl} from '@@/plugin-locale/localeExports';
+import { useIntl } from '@@/plugin-locale/localeExports';
 import Import from './Import';
-import type {ReactNode} from 'react';
-import {useState} from 'react';
+import type { ReactNode } from 'react';
+import { useState } from 'react';
 import Cat from './Cat';
 import Service from '@/pages/device/components/Metadata/service';
-import {InfoCircleOutlined} from '@ant-design/icons';
+import { InfoCircleOutlined } from '@ant-design/icons';
 import styles from './index.less';
-import {InstanceModel} from '@/pages/device/Instance';
-import {PermissionButton} from '@/components';
+import { InstanceModel } from '@/pages/device/Instance';
+import { PermissionButton } from '@/components';
 
 interface Props {
   tabAction?: ReactNode;

+ 44 - 35
src/pages/media/Device/Playback/index.tsx

@@ -26,19 +26,22 @@ export default () => {
   const [type, setType] = useState('local');
   const [historyList, setHistoryList] = useState<recordsItemType[]>([]);
   const [time, setTime] = useState<Moment | undefined>(undefined);
-  const [playTime, setPlayTime] = useState(0);
   // const [loading, setLoading] = useState(false)
   const [cloudTime, setCloudTime] = useState<any>();
-  const [playing, setPlaying] = useState(false);
   const location = useLocation();
   const player = useRef<any>();
+  const [playStatus, setPlayStatus] = useState(0); // 播放状态, 0 停止, 1 播放, 2 暂停, 3 播放完成
+  const [playTime, setPlayTime] = useState(0);
 
+  const playNowTime = useRef<number>(0); // 当前播放视频标识
+  const playTimeNode = useRef<any>(null);
+  const isEnded = useRef(false); // 是否结束播放
   const param = new URLSearchParams(location.search);
   const deviceId = param.get('id');
   const channelId = param.get('channelId');
 
   const queryLocalRecords = async (date: Moment) => {
-    setPlaying(false);
+    setPlayStatus(0);
     setUrl('');
     if (deviceId && channelId && date) {
       const params = {
@@ -73,7 +76,7 @@ export default () => {
   };
 
   const queryServiceRecords = async (date: Moment) => {
-    setPlaying(false);
+    setPlayStatus(0);
     setUrl('');
     if (deviceId && channelId && date) {
       const params = {
@@ -189,25 +192,35 @@ export default () => {
             live={type === 'local'}
             ref={player}
             onPlay={() => {
-              setPlaying(true);
+              isEnded.current = false;
+              setPlayStatus(1);
             }}
             onPause={() => {
-              setPlaying(false);
+              setPlayStatus(2);
             }}
-            onDestroy={() => {
-              setPlaying(false);
+            onEnded={() => {
+              setPlayStatus(0);
+              if (playTimeNode.current && !isEnded.current) {
+                isEnded.current = true;
+                playTimeNode.current.onNextPlay();
+              }
             }}
             onError={() => {
-              setPlaying(false);
+              setPlayStatus(0);
+            }}
+            onTimeUpdate={(e) => {
+              setPlayTime(parseInt(e.detail[0]));
             }}
           />
           <TimeLine
+            ref={playTimeNode}
             type={type}
             data={historyList}
             dateTime={time}
             onChange={(times) => {
               if (times) {
-                setPlayTime(Number(times.endTime.valueOf()));
+                playNowTime.current = Number(times.startTime.valueOf());
+                setPlayTime(0);
                 setUrl(
                   type === 'local'
                     ? service.playbackLocal(
@@ -223,7 +236,8 @@ export default () => {
                 setUrl('');
               }
             }}
-            playing={playing}
+            playStatus={playStatus}
+            playTime={playNowTime.current + playTime * 1000}
             localToServer={cloudTime}
           />
         </div>
@@ -266,48 +280,43 @@ export default () => {
                 itemLayout="horizontal"
                 dataSource={historyList}
                 renderItem={(item) => {
+                  const _startTime = item.startTime || item.mediaStartTime;
                   const startTime = moment(item.startTime || item.mediaStartTime).format(
                     'HH:mm:ss',
                   );
                   const endTime = moment(item.endTime || item.mediaEndTime).format('HH:mm:ss');
                   const downloadObj = DownloadIcon(item);
-                  const timeId = item.endTime || item.mediaEndTime;
-
-                  console.log(timeId, playTime);
                   return (
                     <List.Item
                       actions={[
                         <Tooltip
                           key="play-btn"
-                          title={item.startTime === playTime ? '暂停' : '播放'}
+                          title={
+                            _startTime === playNowTime.current && playStatus === 1 ? '暂停' : '播放'
+                          }
                         >
                           <a
                             onClick={() => {
-                              if (!playTime) {
-                                setPlayTime(item.startTime);
-                                if (item.filePath) {
-                                  service.playbackStart(item.id);
-                                } else if (deviceId && channelId) {
-                                  setUrl(
-                                    service.playbackLocal(
-                                      deviceId,
-                                      channelId,
-                                      'mp4',
-                                      moment(item.startTime).format('YYYY-MM-DD HH:mm:ss'),
-                                      moment(item.endTime).format('YYYY-MM-DD HH:mm:ss'),
-                                    ),
-                                  );
+                              if (playStatus === 0 || _startTime !== playNowTime.current) {
+                                if (playTimeNode.current) {
+                                  playTimeNode.current.playByStartTime(_startTime);
+                                }
+                              } else if (playStatus == 1 && _startTime === playNowTime.current) {
+                                if (player.current.getVueInstance) {
+                                  player.current.getVueInstance().pause();
                                 }
-                              } else {
-                                console.log(player.current);
-                                if (player.current.pause) {
-                                  player.current.pause();
+                              } else if (playStatus == 2 && _startTime === playNowTime.current) {
+                                if (player.current.getVueInstance) {
+                                  player.current.getVueInstance().play();
                                 }
-                                setPlayTime(0);
                               }
                             }}
                           >
-                            {timeId === playTime ? <PauseCircleOutlined /> : <PlayCircleOutlined />}
+                            {_startTime === playNowTime.current && playStatus === 1 ? (
+                              <PauseCircleOutlined />
+                            ) : (
+                              <PlayCircleOutlined />
+                            )}
                           </a>
                         </Tooltip>,
                         <Tooltip key={'download'} title={downloadObj.title}>

+ 56 - 37
src/pages/media/Device/Playback/timeLine.tsx

@@ -1,7 +1,7 @@
 import { message } from 'antd';
 import moment from 'moment';
 import type { Moment } from 'moment';
-import { useEffect, useState, useRef } from 'react';
+import { useEffect, useState, useRef, useCallback, forwardRef, useImperativeHandle } from 'react';
 import './index.less';
 import { recordsItemType } from '@/pages/media/Device/Playback/typings';
 import { useSize } from 'ahooks';
@@ -19,7 +19,8 @@ interface Props {
   data: recordsItemType[];
   dateTime?: Moment;
   type: string;
-  playing: boolean;
+  playStatus: number;
+  playTime: number;
   server?: any;
   localToServer?: {
     endTime: number;
@@ -28,7 +29,7 @@ interface Props {
   getPlayList?: (data: any) => void;
 }
 
-const Progress = (props: Props) => {
+const Progress = forwardRef((props: Props, ref) => {
   const [startT, setStartT] = useState<number>(
     new Date(moment(props.dateTime).startOf('day').format('YYYY-MM-DD HH:mm:ss')).getTime(),
   ); // 获取选中当天开始时间戳
@@ -37,7 +38,7 @@ const Progress = (props: Props) => {
   ).getTime(); // 获取选中当天结束时间戳
 
   const [list, setList] = useState<any[]>([]);
-  const [time, setTime] = useState<number>(startT);
+  const [playTime, setPlayTime] = useState<number>(0);
 
   const LineContent = useRef<HTMLDivElement>(null);
   const LineContentSize = useSize(LineContent);
@@ -61,6 +62,7 @@ const Progress = (props: Props) => {
   }, [props.dateTime]);
 
   const onChange = (startTime: number, endTime: number, deviceId: string, channelId: string) => {
+    setPlayTime(startTime);
     props.onChange({
       startTime: moment(startTime),
       endTime: moment(endTime),
@@ -69,6 +71,41 @@ const Progress = (props: Props) => {
     });
   };
 
+  const playByStartTime = useCallback(
+    (time) => {
+      const playNow = props.data.find((item) => {
+        const startTime = item.startTime || item.mediaStartTime;
+        return startTime === time;
+      });
+
+      if (playNow) {
+        const startTime = playNow.startTime || playNow.mediaStartTime;
+        const endTime = playNow.endTime || playNow.mediaEndTime;
+        const deviceId = props.type === 'local' ? playNow.deviceId : playNow.id;
+        onChange(startTime, endTime, deviceId, playNow.channelId);
+      }
+    },
+    [props.type, props.data],
+  );
+
+  const onNextPlay = useCallback(() => {
+    if (playTime) {
+      // 查找下一个视频
+      const nowIndex = props.data.findIndex((item) => {
+        const startTime = item.startTime || item.mediaStartTime;
+        return startTime === playTime;
+      });
+      // 是否为最后一个
+      if (nowIndex !== props.data.length - 1) {
+        const nextPlay = props.data[nowIndex + 1];
+        const startTime = nextPlay.startTime || nextPlay.mediaStartTime;
+        const endTime = nextPlay.endTime || nextPlay.mediaEndTime;
+        const deviceId = props.type === 'local' ? nextPlay.deviceId : nextPlay.id;
+        onChange(startTime, endTime, deviceId, nextPlay.channelId);
+      }
+    }
+  }, [props.type, playTime, props.data]);
+
   useEffect(() => {
     const { data, localToServer, type } = props;
     if (data && Array.isArray(data) && data.length > 0) {
@@ -76,7 +113,6 @@ const Progress = (props: Props) => {
       if (type === 'local') {
         // 播放第一个
         onChange(data[0].startTime, data[0].endTime, data[0].deviceId, data[0].channelId);
-        setTime(startT);
       } else if (type === 'cloud') {
         // 是否从本地跳转到云端播放
         if (localToServer && Object.keys(localToServer).length > 0) {
@@ -95,14 +131,11 @@ const Progress = (props: Props) => {
               playItem.id,
               playItem.channelId,
             );
-            setTime(playItem.mediaStartTime);
           } else {
             props.onChange(undefined);
-            setTime(localToServer.startTime);
             message.error('没有可播放的视频资源');
           }
         } else {
-          setTime(data[0].mediaStartTime);
           onChange(data[0].mediaStartTime, data[0].mediaEndTime, data[0].id, data[0].channelId);
         }
       }
@@ -110,28 +143,14 @@ const Progress = (props: Props) => {
       // 本地跳转云端但是无资源
       props.onChange(undefined);
       message.error('没有可播放的视频资源');
-      setTime(startT);
       setList([]);
     } else {
       // 啥都没有
-      setTime(startT);
       setList([]);
       props.onChange(undefined);
     }
   }, [props.data]);
 
-  useEffect(() => {
-    // if(props.server && Object.keys(props.server).length > 0){
-    //   if(props.type === 'local'){
-    //     setTime(props.server.startTime)
-    //     props.play({ start: props.server.startTime, end: props.server.endTime })
-    //   } else {
-    //     setTime(props.server.mediaStartTime)
-    //     props.play(props.server)
-    //   }
-    // }
-  }, [props.server]);
-
   const getLineItemStyle = (
     startTime: number,
     endTime: number,
@@ -146,21 +165,21 @@ const Progress = (props: Props) => {
   };
 
   useEffect(() => {
-    let timerId: any = null;
-    if (props.playing) {
-      timerId = setInterval(() => {
-        // eslint-disable-next-line @typescript-eslint/no-shadow
-        setTime((time) => time + 1000);
-      }, 1000);
+    if (
+      props.playTime &&
+      props.playTime >= startT &&
+      props.playTime <= endT &&
+      props.data &&
+      props.data.length
+    ) {
+      setTimeAndPosition((props.playTime - startT) / 3600000 / 24);
     }
-    return () => timerId && clearInterval(timerId);
-  }, [props.playing]);
+  }, [props.playTime, startT]);
 
-  useEffect(() => {
-    if (time >= startT && time <= endT && props.data && props.data.length) {
-      setTimeAndPosition((time - startT) / 3600000 / 24);
-    }
-  }, [time]);
+  useImperativeHandle(ref, () => ({
+    onNextPlay,
+    playByStartTime,
+  }));
 
   return (
     <div className={'time-line-warp'}>
@@ -196,10 +215,10 @@ const Progress = (props: Props) => {
         </div>
         <div id="btn" className={classNames('time-line-btn')}></div>
         <div id="time" className={classNames('time-line')}>
-          {moment(time).format('HH:mm:ss')}
+          {moment(props.playTime || 0).format('HH:mm:ss')}
         </div>
       </div>
     </div>
   );
-};
+});
 export default Progress;

+ 1 - 1
src/pages/media/Device/Save/ProviderSelect.tsx

@@ -61,7 +61,7 @@ export default (props: ProviderProps) => {
     tab!.onTabSaveSuccess = (value: any) => {
       addItemKey.current = value.id;
       getProviderList({
-        sorts: [{ name: 'createTime', value: 'asc' }],
+        sorts: [{ name: 'createTime', order: 'desc' }],
         terms: [{ column: 'provider', value: props.type }],
         pageSize: 100,
       });

+ 1 - 1
src/pages/media/Device/Save/SaveProduct.tsx

@@ -24,7 +24,7 @@ export default (props: SaveProps) => {
   useEffect(() => {
     if (visible) {
       getProviderList({
-        sorts: [{ name: 'createTime', value: 'desc' }],
+        sorts: [{ name: 'createTime', order: 'desc' }],
         terms: [{ column: 'provider', value: props.type }],
         pageSize: 100,
       });

+ 1 - 0
src/pages/rule-engine/Scene/Save/index.tsx

@@ -0,0 +1 @@
+export default () => {};

+ 177 - 92
src/pages/rule-engine/Scene/index.tsx

@@ -1,32 +1,125 @@
 import { PageContainer } from '@ant-design/pro-layout';
-import BaseService from '@/utils/BaseService';
-import { useRef } from 'react';
+import { useRef, useState } from 'react';
 import type { ActionType, ProColumns } from '@jetlinks/pro-table';
 import type { SceneItem } from '@/pages/rule-engine/Scene/typings';
-import { Tooltip } from 'antd';
-import {
-  CaretRightOutlined,
-  EditOutlined,
-  EyeOutlined,
-  MinusOutlined,
-  ReloadOutlined,
-  StopOutlined,
-} from '@ant-design/icons';
-import BaseCrud from '@/components/BaseCrud';
+import { Badge, message } from 'antd';
+import { EditOutlined, PlayCircleOutlined, PlusOutlined, StopOutlined } from '@ant-design/icons';
 import { useIntl } from '@@/plugin-locale/localeExports';
+import { PermissionButton, ProTableCard } from '@/components';
+import { statusMap } from '@/pages/device/Instance';
+import SearchComponent from '@/components/SearchComponent';
+import Service from './service';
+
+export const service = new Service('rule-engine/scene');
 
-export const service = new BaseService<SceneItem>('rule-engine/scene');
 const Scene = () => {
   const intl = useIntl();
   const actionRef = useRef<ActionType>();
+  const { permission } = PermissionButton.usePermission('rule-engine/Scene');
+  const [searchParams, setSearchParams] = useState<any>({});
+
+  const Tools = (record: any, type: 'card' | 'table') => {
+    return [
+      <PermissionButton
+        key={'update'}
+        type={'link'}
+        style={{ padding: 0 }}
+        isPermission={permission.update}
+        tooltip={
+          type === 'table'
+            ? {
+                title: intl.formatMessage({
+                  id: 'pages.data.option.edit',
+                  defaultMessage: '编辑',
+                }),
+              }
+            : undefined
+        }
+      >
+        <EditOutlined />
+        {type === 'table' &&
+          intl.formatMessage({
+            id: 'pages.data.option.edit',
+            defaultMessage: '编辑',
+          })}
+      </PermissionButton>,
+      <PermissionButton
+        key={'update'}
+        type={'link'}
+        style={{ padding: 0 }}
+        isPermission={permission.action}
+        popConfirm={{
+          title: intl.formatMessage({
+            id: `pages.data.option.${
+              record.state.value !== 'notActive' ? 'disabled' : 'enabled'
+            }.tips`,
+            defaultMessage: '确认禁用?',
+          }),
+          onConfirm: async () => {
+            message.success(
+              intl.formatMessage({
+                id: 'pages.data.option.success',
+                defaultMessage: '操作成功!',
+              }),
+            );
+            actionRef.current?.reload();
+          },
+        }}
+        tooltip={
+          type === 'table'
+            ? {
+                title: intl.formatMessage({
+                  id: 'pages.data.option.edit',
+                  defaultMessage: '编辑',
+                }),
+              }
+            : undefined
+        }
+      >
+        {record.state.value !== 'notActive' ? <StopOutlined /> : <PlayCircleOutlined />}
+        {type === 'table' &&
+          intl.formatMessage({
+            id: `pages.data.option.${record.state.value !== 'notActive' ? 'disabled' : 'enabled'}`,
+            defaultMessage: record.state.value !== 'notActive' ? '禁用' : '启用',
+          })}
+      </PermissionButton>,
+      <PermissionButton
+        key={'delete'}
+        type={'link'}
+        style={{ padding: 0 }}
+        isPermission={permission.delete}
+        popConfirm={{
+          title: intl.formatMessage({
+            id:
+              record.state.value === 'notActive'
+                ? 'pages.data.option.remove.tips'
+                : 'pages.device.instance.deleteTip',
+          }),
+          onConfirm: async () => {},
+        }}
+        tooltip={
+          type === 'table'
+            ? {
+                title: intl.formatMessage({
+                  id: 'pages.data.option.edit',
+                  defaultMessage: '编辑',
+                }),
+              }
+            : undefined
+        }
+      >
+        <EditOutlined />
+        {type === 'table' &&
+          intl.formatMessage({
+            id: 'pages.data.option.edit',
+            defaultMessage: '编辑',
+          })}
+      </PermissionButton>,
+    ];
+  };
 
   const columns: ProColumns<SceneItem>[] = [
     {
-      dataIndex: 'index',
-      valueType: 'indexBorder',
-      width: 48,
-    },
-    {
       dataIndex: 'name',
       title: intl.formatMessage({
         id: 'pages.table.name',
@@ -39,7 +132,13 @@ const Scene = () => {
         id: 'pages.ruleEngine.scene.triggers',
         defaultMessage: '触发方式',
       }),
-      render: () => 'todo',
+    },
+    {
+      dataIndex: 'describe',
+      title: intl.formatMessage({
+        id: 'pages.system.description',
+        defaultMessage: '说明',
+      }),
     },
     {
       dataIndex: 'state',
@@ -47,7 +146,26 @@ const Scene = () => {
         id: 'pages.searchTable.titleStatus',
         defaultMessage: '状态',
       }),
-      render: (text, record) => record.state.value,
+      width: '90px',
+      valueType: 'select',
+      renderText: (record) =>
+        record ? <Badge status={statusMap.get(record.value)} text={record.text} /> : '',
+      valueEnum: {
+        offline: {
+          text: intl.formatMessage({
+            id: 'pages.device.instance.status.offLine',
+            defaultMessage: '离线',
+          }),
+          status: 'offline',
+        },
+        online: {
+          text: intl.formatMessage({
+            id: 'pages.device.instance.status.onLine',
+            defaultMessage: '在线',
+          }),
+          status: 'online',
+        },
+      },
     },
     {
       title: intl.formatMessage({
@@ -57,85 +175,52 @@ const Scene = () => {
       valueType: 'option',
       align: 'center',
       width: 200,
-      render: (text, record) => [
-        <a>
-          <Tooltip
-            title={intl.formatMessage({
-              id: 'pages.ruleEngine.option.detail',
-              defaultMessage: '查看',
-            })}
-          >
-            <EyeOutlined />
-          </Tooltip>
-        </a>,
-        <a onClick={() => console.log(record)}>
-          <Tooltip
-            title={intl.formatMessage({
-              id: 'pages.data.option.edit',
-              defaultMessage: '编辑',
-            })}
-          >
-            <EditOutlined />
-          </Tooltip>
-        </a>,
-        <a onClick={() => console.log(record)}>
-          <Tooltip
-            title={intl.formatMessage({
-              id: 'pages.ruleEngine.option.start',
-              defaultMessage: '启动',
-            })}
-          >
-            <CaretRightOutlined />
-          </Tooltip>
-        </a>,
-        <a onClick={() => console.log(record)}>
-          <Tooltip
-            title={intl.formatMessage({
-              id: 'pages.ruleEngine.option.stop',
-              defaultMessage: '停止',
-            })}
-          >
-            <StopOutlined />
-          </Tooltip>
-        </a>,
-        <a onClick={() => console.log(record)}>
-          <Tooltip
-            title={intl.formatMessage({
-              id: 'pages.ruleEngine.option.restart',
-              defaultMessage: '重启',
-            })}
-          >
-            <ReloadOutlined />
-          </Tooltip>
-        </a>,
-
-        <a>
-          <Tooltip
-            title={intl.formatMessage({
-              id: 'pages.data.option.remove',
-              defaultMessage: '删除',
-            })}
-          >
-            <MinusOutlined />
-          </Tooltip>
-        </a>,
-      ],
+      render: (text, record) => Tools(record, 'table'),
     },
   ];
 
-  const schema = {};
-
   return (
     <PageContainer>
-      <BaseCrud
+      <SearchComponent
+        field={columns}
+        target={'rule-engine-scene'}
+        onSearch={(data) => {
+          actionRef.current?.reset?.();
+          setSearchParams(data);
+        }}
+      />
+      <ProTableCard
         columns={columns}
-        service={service}
-        title={intl.formatMessage({
-          id: 'pages.ruleEngine.scene',
-          defaultMessage: '场景联动',
-        })}
-        schema={schema}
         actionRef={actionRef}
+        params={searchParams}
+        options={{ fullScreen: true }}
+        request={(params) =>
+          service.query({
+            ...params,
+            sorts: [
+              {
+                name: 'createTime',
+                order: 'desc',
+              },
+            ],
+          })
+        }
+        rowKey="id"
+        search={false}
+        headerTitle={[
+          <PermissionButton
+            key="button"
+            icon={<PlusOutlined />}
+            type="primary"
+            isPermission={permission.add}
+            onClick={() => {}}
+          >
+            {intl.formatMessage({
+              id: 'pages.data.option.add',
+              defaultMessage: '新增',
+            })}
+          </PermissionButton>,
+        ]}
       />
     </PageContainer>
   );

+ 9 - 0
src/pages/rule-engine/Scene/service.ts

@@ -0,0 +1,9 @@
+import { request } from '@@/plugin-request/request';
+import BaseService from '@/utils/BaseService';
+import type { SceneItem } from '@/pages/rule-engine/Scene/typings';
+
+class Service extends BaseService<SceneItem> {
+  start = (id: string) => request(`${this.uri}/${id}`, { methods: 'GET' });
+}
+
+export default Service;

+ 1 - 0
src/pages/rule-engine/Scene/typings.d.ts

@@ -9,6 +9,7 @@ type Trigger = {
   trigger: string;
   device: Record<string, unknown>;
 };
+
 type SceneItem = {
   parallel: boolean;
   state: State;

+ 13 - 9
src/pages/system/Menu/Detail/buttons.tsx

@@ -1,14 +1,14 @@
-import {Button, Form, Input, message, Modal, Tooltip} from 'antd';
-import {useIntl} from '@@/plugin-locale/localeExports';
-import {useCallback, useEffect, useState} from 'react';
-import {service} from '@/pages/system/Menu';
-import type {ProColumns} from '@jetlinks/pro-table';
+import { Button, Form, Input, message, Modal, Tooltip } from 'antd';
+import { useIntl } from '@@/plugin-locale/localeExports';
+import { useCallback, useEffect, useState } from 'react';
+import { service } from '@/pages/system/Menu';
+import type { ProColumns } from '@jetlinks/pro-table';
 import ProTable from '@jetlinks/pro-table';
-import {DeleteOutlined, EditOutlined, PlusOutlined, SearchOutlined} from '@ant-design/icons';
-import type {MenuButtonInfo, MenuItem} from '@/pages/system/Menu/typing';
+import { DeleteOutlined, EditOutlined, PlusOutlined, SearchOutlined } from '@ant-design/icons';
+import type { MenuButtonInfo, MenuItem } from '@/pages/system/Menu/typing';
 import Permission from '@/pages/system/Menu/components/permission';
-import {useRequest} from '@@/plugin-request/request';
-import {PermissionButton} from '@/components';
+import { useRequest } from '@@/plugin-request/request';
+import { PermissionButton } from '@/components';
 
 type ButtonsProps = {
   data: MenuItem;
@@ -23,6 +23,7 @@ export default (props: ButtonsProps) => {
   const [visible, setVisible] = useState(false); // Modal显示影藏
   const [id, setId] = useState(''); // 缓存ID
   const [form] = Form.useForm();
+  const [loading, setLoading] = useState(false);
   const { permission } = PermissionButton.usePermission('system/Menu');
 
   const { data: permissions, run: queryPermissions } = useRequest(service.queryPermission, {
@@ -67,10 +68,12 @@ export default (props: ButtonsProps) => {
   const updateMenuInfo = useCallback(
     async (data: MenuButtonInfo[]) => {
       if (props.data.id) {
+        setLoading(true);
         const response = await service.update({
           ...props.data,
           buttons: data,
         });
+        setLoading(false);
         if (response.status === 200) {
           message.success('操作成功!');
           props.onLoad();
@@ -283,6 +286,7 @@ export default (props: ButtonsProps) => {
           resetForm();
           setVisible(false);
         }}
+        confirmLoading={loading}
       >
         <Form form={form} layout={'vertical'}>
           <Form.Item

+ 32 - 10
src/pages/system/Menu/Detail/edit.tsx

@@ -1,16 +1,28 @@
-import {Card, Col, Form, Input, InputNumber, message, Radio, Row, Select, Tooltip, TreeSelect,} from 'antd';
+import {
+  Card,
+  Col,
+  Form,
+  Input,
+  InputNumber,
+  message,
+  Radio,
+  Row,
+  Select,
+  Tooltip,
+  TreeSelect,
+} from 'antd';
 import Permission from '@/pages/system/Menu/components/permission';
-import {useIntl} from '@@/plugin-locale/localeExports';
-import {useEffect, useState} from 'react';
-import {service} from '@/pages/system/Menu';
-import {useHistory, useRequest} from 'umi';
-import type {MenuItem} from '@/pages/system/Menu/typing';
+import { useIntl } from '@@/plugin-locale/localeExports';
+import { useEffect, useState } from 'react';
+import { service } from '@/pages/system/Menu';
+import { useHistory, useRequest } from 'umi';
+import type { MenuItem } from '@/pages/system/Menu/typing';
 // import { debounce } from 'lodash';
 import Title from '../components/Title';
 import Icons from '../components/Icons';
-import {QuestionCircleFilled} from '@ant-design/icons';
-import {getMenuPathByParams, MENUS_CODE} from '@/utils/menu';
-import {PermissionButton} from '@/components';
+import { QuestionCircleFilled } from '@ant-design/icons';
+import { getMenuPathByParams, MENUS_CODE } from '@/utils/menu';
+import { PermissionButton } from '@/components';
 
 type EditProps = {
   data: MenuItem;
@@ -22,6 +34,7 @@ export default (props: EditProps) => {
   const intl = useIntl();
   const [disabled, setDisabled] = useState(true);
   const [show] = useState(true);
+  const [loading, setLoading] = useState(false);
   const [accessSupport, setAccessSupport] = useState('unsupported');
   const history = useHistory();
   const { getOtherPermission } = PermissionButton.usePermission('system/Menu');
@@ -59,9 +72,11 @@ export default (props: EditProps) => {
       //   switch: show,
       // };
 
+      setLoading(true);
       const response: any = !formData.id
         ? await service.save(formData)
         : await service.update(formData);
+      setLoading(false);
       if (response.status === 200) {
         message.success('操作成功!');
         setDisabled(true);
@@ -205,7 +220,13 @@ export default (props: EditProps) => {
             </Col>
             <Col span={24}>
               <Form.Item name={'describe'} label={'说明'}>
-                <Input.TextArea rows={4} maxLength={200} showCount placeholder={'请输入说明'} />
+                <Input.TextArea
+                  disabled={disabled}
+                  rows={4}
+                  maxLength={200}
+                  showCount
+                  placeholder={'请输入说明'}
+                />
               </Form.Item>
             </Col>
           </Row>
@@ -316,6 +337,7 @@ export default (props: EditProps) => {
                 saveData();
               }
             }}
+            loading={loading}
             isPermission={getOtherPermission(['add', 'update'])}
           >
             {intl.formatMessage({