Parcourir la source

fix(merge): merge xyh

lind il y a 3 ans
Parent
commit
360021de05

+ 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 { forwardRef, useEffect, useImperativeHandle, useRef, 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 || ''}
     />
   );
 });

+ 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>
   );

+ 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');
             },

+ 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}>

+ 57 - 38
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 moment from 'moment';
+import { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } 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({