Jelajahi Sumber

feat(merge): merge xyh

Next xyh
Lind 3 tahun lalu
induk
melakukan
4e72bcbd11

+ 141 - 135
src/components/Player/ScreenPlayer.tsx

@@ -4,17 +4,9 @@ import LivePlayer from './index';
 import { Button, Dropdown, Empty, Input, Menu, Popover, Radio, Tooltip } from 'antd';
 import { useFullscreen } from 'ahooks';
 import './index.less';
-import {
-  CaretDownOutlined,
-  CaretLeftOutlined,
-  CaretRightOutlined,
-  CaretUpOutlined,
-  DeleteOutlined,
-  MinusOutlined,
-  PlusOutlined,
-  QuestionCircleOutlined,
-} from '@ant-design/icons';
+import { DeleteOutlined, QuestionCircleOutlined } from '@ant-design/icons';
 import Service from './service';
+import MediaTool from '@/components/Player/mediaTool';
 
 type Player = {
   id?: string;
@@ -264,131 +256,145 @@ export default forwardRef((props: ScreenProps, ref) => {
           </div>
         </div>
       </div>
-      <div className={'live-player-tools'}>
-        <div className={'direction'}>
-          <div
-            className={'direction-item up'}
-            onMouseDown={() => {
-              const { id, channelId } = players[playerActive];
-              if (id && channelId && props.onMouseDown) {
-                props.onMouseDown(id, channelId, 'UP');
-              }
-            }}
-            onMouseUp={() => {
-              const { id, channelId } = players[playerActive];
-              if (props.onMouseUp && id && channelId) {
-                props.onMouseUp(id, channelId, 'UP');
-              }
-            }}
-          >
-            <CaretUpOutlined className={'direction-icon'} />
-          </div>
-          <div
-            className={'direction-item right'}
-            onMouseDown={() => {
-              const { id, channelId } = players[playerActive];
-              if (props.onMouseDown && id && channelId) {
-                props.onMouseDown(id, channelId, 'RIGHT');
-              }
-            }}
-            onMouseUp={() => {
-              const { id, channelId } = players[playerActive];
-              if (props.onMouseUp && id && channelId) {
-                props.onMouseUp(id, channelId, 'RIGHT');
-              }
-            }}
-          >
-            <CaretRightOutlined className={'direction-icon'} />
-          </div>
-          <div
-            className={'direction-item left'}
-            onMouseDown={() => {
-              const { id, channelId } = players[playerActive];
-              if (props.onMouseDown && id && channelId) {
-                props.onMouseDown(id, channelId, 'LEFT');
-              }
-            }}
-            onMouseUp={() => {
-              const { id, channelId } = players[playerActive];
-              if (props.onMouseUp && id && channelId) {
-                props.onMouseUp(id, channelId, 'LEFT');
-              }
-            }}
-          >
-            <CaretLeftOutlined className={'direction-icon'} />
-          </div>
-          <div
-            className={'direction-item down'}
-            onMouseDown={() => {
-              const { id, channelId } = players[playerActive];
-              if (props.onMouseDown && id && channelId) {
-                props.onMouseDown(id, channelId, 'DOWN');
-              }
-            }}
-            onMouseUp={() => {
-              const { id, channelId } = players[playerActive];
-              if (props.onMouseUp && id && channelId) {
-                props.onMouseUp(id, channelId, 'DOWN');
-              }
-            }}
-          >
-            <CaretDownOutlined className={'direction-icon'} />
-          </div>
-          <div
-            className={'direction-audio'}
-            // onMouseDown={() => {
-            //   const { id, channelId } = players[playerActive];
-            //   if (props.onMouseDown && id && channelId) {
-            //     props.onMouseDown(id, channelId, 'AUDIO');
-            //   }
-            // }}
-            // onMouseUp={() => {
-            //   const { id, channelId } = players[playerActive];
-            //   if (props.onMouseUp && id && channelId) {
-            //     props.onMouseUp(id, channelId, 'AUDIO');
-            //   }
-            // }}
-          >
-            {/*<AudioOutlined />*/}
-          </div>
-        </div>
-        <div className={'zoom'}>
-          <div
-            className={'zoom-item zoom-in'}
-            onMouseDown={() => {
-              const { id, channelId } = players[playerActive];
-              if (props.onMouseDown && id && channelId) {
-                props.onMouseDown(id, channelId, 'ZOOM_IN');
-              }
-            }}
-            onMouseUp={() => {
-              const { id, channelId } = players[playerActive];
-              if (props.onMouseUp && id && channelId) {
-                props.onMouseUp(id, channelId, 'ZOOM_IN');
-              }
-            }}
-          >
-            <PlusOutlined />
-          </div>
-          <div
-            className={'zoom-item zoom-out'}
-            onMouseDown={() => {
-              const { id, channelId } = players[playerActive];
-              if (props.onMouseDown && id && channelId) {
-                props.onMouseDown(id, channelId, 'ZOOM_OUT');
-              }
-            }}
-            onMouseUp={() => {
-              const { id, channelId } = players[playerActive];
-              if (props.onMouseUp && id && channelId) {
-                props.onMouseUp(id, channelId, 'ZOOM_OUT');
-              }
-            }}
-          >
-            <MinusOutlined />
-          </div>
-        </div>
-      </div>
+      <MediaTool
+        onMouseDown={(type) => {
+          const { id, channelId } = players[playerActive];
+          if (id && channelId && props.onMouseDown) {
+            props.onMouseDown(id, channelId, type);
+          }
+        }}
+        onMouseUp={(type) => {
+          const { id, channelId } = players[playerActive];
+          if (props.onMouseUp && id && channelId) {
+            props.onMouseUp(id, channelId, type);
+          }
+        }}
+      />
+      {/*<div className={'live-player-tools'}>*/}
+      {/*  <div className={'direction'}>*/}
+      {/*    <div*/}
+      {/*      className={'direction-item up'}*/}
+      {/*      onMouseDown={() => {*/}
+      {/*        const { id, channelId } = players[playerActive];*/}
+      {/*        if (id && channelId && props.onMouseDown) {*/}
+      {/*          props.onMouseDown(id, channelId, 'UP');*/}
+      {/*        }*/}
+      {/*      }}*/}
+      {/*      onMouseUp={() => {*/}
+      {/*        const { id, channelId } = players[playerActive];*/}
+      {/*        if (props.onMouseUp && id && channelId) {*/}
+      {/*          props.onMouseUp(id, channelId, 'UP');*/}
+      {/*        }*/}
+      {/*      }}*/}
+      {/*    >*/}
+      {/*      <CaretUpOutlined className={'direction-icon'} />*/}
+      {/*    </div>*/}
+      {/*    <div*/}
+      {/*      className={'direction-item right'}*/}
+      {/*      onMouseDown={() => {*/}
+      {/*        const { id, channelId } = players[playerActive];*/}
+      {/*        if (props.onMouseDown && id && channelId) {*/}
+      {/*          props.onMouseDown(id, channelId, 'RIGHT');*/}
+      {/*        }*/}
+      {/*      }}*/}
+      {/*      onMouseUp={() => {*/}
+      {/*        const { id, channelId } = players[playerActive];*/}
+      {/*        if (props.onMouseUp && id && channelId) {*/}
+      {/*          props.onMouseUp(id, channelId, 'RIGHT');*/}
+      {/*        }*/}
+      {/*      }}*/}
+      {/*    >*/}
+      {/*      <CaretRightOutlined className={'direction-icon'} />*/}
+      {/*    </div>*/}
+      {/*    <div*/}
+      {/*      className={'direction-item left'}*/}
+      {/*      onMouseDown={() => {*/}
+      {/*        const { id, channelId } = players[playerActive];*/}
+      {/*        if (props.onMouseDown && id && channelId) {*/}
+      {/*          props.onMouseDown(id, channelId, 'LEFT');*/}
+      {/*        }*/}
+      {/*      }}*/}
+      {/*      onMouseUp={() => {*/}
+      {/*        const { id, channelId } = players[playerActive];*/}
+      {/*        if (props.onMouseUp && id && channelId) {*/}
+      {/*          props.onMouseUp(id, channelId, 'LEFT');*/}
+      {/*        }*/}
+      {/*      }}*/}
+      {/*    >*/}
+      {/*      <CaretLeftOutlined className={'direction-icon'} />*/}
+      {/*    </div>*/}
+      {/*    <div*/}
+      {/*      className={'direction-item down'}*/}
+      {/*      onMouseDown={() => {*/}
+      {/*        const { id, channelId } = players[playerActive];*/}
+      {/*        if (props.onMouseDown && id && channelId) {*/}
+      {/*          props.onMouseDown(id, channelId, 'DOWN');*/}
+      {/*        }*/}
+      {/*      }}*/}
+      {/*      onMouseUp={() => {*/}
+      {/*        const { id, channelId } = players[playerActive];*/}
+      {/*        if (props.onMouseUp && id && channelId) {*/}
+      {/*          props.onMouseUp(id, channelId, 'DOWN');*/}
+      {/*        }*/}
+      {/*      }}*/}
+      {/*    >*/}
+      {/*      <CaretDownOutlined className={'direction-icon'} />*/}
+      {/*    </div>*/}
+      {/*    <div*/}
+      {/*      className={'direction-audio'}*/}
+      {/*      // onMouseDown={() => {*/}
+      {/*      //   const { id, channelId } = players[playerActive];*/}
+      {/*      //   if (props.onMouseDown && id && channelId) {*/}
+      {/*      //     props.onMouseDown(id, channelId, 'AUDIO');*/}
+      {/*      //   }*/}
+      {/*      // }}*/}
+      {/*      // onMouseUp={() => {*/}
+      {/*      //   const { id, channelId } = players[playerActive];*/}
+      {/*      //   if (props.onMouseUp && id && channelId) {*/}
+      {/*      //     props.onMouseUp(id, channelId, 'AUDIO');*/}
+      {/*      //   }*/}
+      {/*      // }}*/}
+      {/*    >*/}
+      {/*      /!*<AudioOutlined />*!/*/}
+      {/*    </div>*/}
+      {/*  </div>*/}
+      {/*  <div className={'zoom'}>*/}
+      {/*    <div*/}
+      {/*      className={'zoom-item zoom-in'}*/}
+      {/*      onMouseDown={() => {*/}
+      {/*        const { id, channelId } = players[playerActive];*/}
+      {/*        if (props.onMouseDown && id && channelId) {*/}
+      {/*          props.onMouseDown(id, channelId, 'ZOOM_IN');*/}
+      {/*        }*/}
+      {/*      }}*/}
+      {/*      onMouseUp={() => {*/}
+      {/*        const { id, channelId } = players[playerActive];*/}
+      {/*        if (props.onMouseUp && id && channelId) {*/}
+      {/*          props.onMouseUp(id, channelId, 'ZOOM_IN');*/}
+      {/*        }*/}
+      {/*      }}*/}
+      {/*    >*/}
+      {/*      <PlusOutlined />*/}
+      {/*    </div>*/}
+      {/*    <div*/}
+      {/*      className={'zoom-item zoom-out'}*/}
+      {/*      onMouseDown={() => {*/}
+      {/*        const { id, channelId } = players[playerActive];*/}
+      {/*        if (props.onMouseDown && id && channelId) {*/}
+      {/*          props.onMouseDown(id, channelId, 'ZOOM_OUT');*/}
+      {/*        }*/}
+      {/*      }}*/}
+      {/*      onMouseUp={() => {*/}
+      {/*        const { id, channelId } = players[playerActive];*/}
+      {/*        if (props.onMouseUp && id && channelId) {*/}
+      {/*          props.onMouseUp(id, channelId, 'ZOOM_OUT');*/}
+      {/*        }*/}
+      {/*      }}*/}
+      {/*    >*/}
+      {/*      <MinusOutlined />*/}
+      {/*    </div>*/}
+      {/*  </div>*/}
+      {/*</div>*/}
     </div>
   );
 });

+ 0 - 110
src/components/Player/index.less

@@ -1,6 +1,4 @@
 @import '~antd/es/style/themes/default.less';
-@padding: 20px;
-@grid-gap: 2px;
 
 .live-player-warp {
   display: flex;
@@ -59,112 +57,4 @@
       }
     }
   }
-
-  .live-player-tools {
-    display: flex;
-    flex-basis: 250px;
-    flex-direction: column;
-    justify-content: center;
-    margin-left: 24px;
-    padding: 0 12px;
-
-    .direction {
-      position: relative;
-      display: grid;
-      grid-gap: @grid-gap;
-      grid-template-rows: 1fr 1fr;
-      grid-template-columns: 1fr 1fr;
-      margin-bottom: 30px;
-      overflow: hidden;
-      border-radius: 50%;
-      transform: rotateZ(45deg);
-
-      .direction-item {
-        position: relative;
-        display: flex;
-        align-items: center;
-        justify-content: center;
-        padding-bottom: 100%;
-        font-size: 36px;
-        background-color: rgba(#000, 0.1);
-        transition: background-color 0.3s;
-
-        .direction-icon {
-          position: absolute;
-          top: 50%;
-          left: 50%;
-          transform: translate(-50%, -50%) rotateZ(-45deg);
-        }
-      }
-
-      .direction-audio {
-        position: absolute;
-        top: 50%;
-        left: 50%;
-        display: flex;
-        align-items: center;
-        justify-content: center;
-        width: 45%;
-        height: 45%;
-        font-size: 30px;
-        background-color: #fff;
-        border-radius: 50%;
-        transform: translate(-50%, -50%) rotateZ(-45deg);
-      }
-
-      .zoom .zoom-item,
-      & .direction-item {
-        &:hover {
-          color: #fff;
-          background-color: @primary-color-hover;
-        }
-
-        &:active {
-          color: #fff;
-          background-color: @primary-color-active;
-        }
-      }
-
-      > div {
-        cursor: pointer;
-        &.disable {
-          color: @disabled-color;
-        }
-      }
-    }
-
-    .zoom {
-      display: grid;
-      grid-gap: @grid-gap;
-      grid-template-columns: 1fr 1fr;
-
-      .zoom-item {
-        padding: 8px 0;
-        font-size: 24px;
-        text-align: center;
-        background-color: rgba(#000, 0.1);
-        cursor: pointer;
-
-        &:hover {
-          color: #fff;
-          background-color: @primary-color-hover;
-        }
-
-        &:active {
-          color: #fff;
-          background-color: @primary-color-active;
-        }
-      }
-
-      .zoom-in {
-        border-top-left-radius: 4px;
-        border-bottom-left-radius: 4px;
-      }
-
-      .zoom-out {
-        border-top-right-radius: 4px;
-        border-bottom-right-radius: 4px;
-      }
-    }
-  }
 }

+ 111 - 0
src/components/Player/mediaTool.less

@@ -0,0 +1,111 @@
+@import '~antd/es/style/themes/default.less';
+@padding: 20px;
+@grid-gap: 2px;
+
+.live-player-tools {
+  display: flex;
+  flex-basis: 250px;
+  flex-direction: column;
+  justify-content: center;
+  margin-left: 24px;
+  padding: 0 12px;
+
+  .direction {
+    position: relative;
+    display: grid;
+    grid-gap: @grid-gap;
+    grid-template-rows: 1fr 1fr;
+    grid-template-columns: 1fr 1fr;
+    margin-bottom: 30px;
+    overflow: hidden;
+    border-radius: 50%;
+    transform: rotateZ(45deg);
+
+    .direction-item {
+      position: relative;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      padding-bottom: 100%;
+      font-size: 36px;
+      background-color: rgba(#000, 0.1);
+      transition: background-color 0.3s;
+
+      .direction-icon {
+        position: absolute;
+        top: 50%;
+        left: 50%;
+        transform: translate(-50%, -50%) rotateZ(-45deg);
+      }
+    }
+
+    .direction-audio {
+      position: absolute;
+      top: 50%;
+      left: 50%;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      width: 45%;
+      height: 45%;
+      font-size: 30px;
+      background-color: #fff;
+      border-radius: 50%;
+      transform: translate(-50%, -50%) rotateZ(-45deg);
+    }
+
+    .zoom .zoom-item,
+    & .direction-item {
+      &:hover {
+        color: #fff;
+        background-color: @primary-color-hover;
+      }
+
+      &:active {
+        color: #fff;
+        background-color: @primary-color-active;
+      }
+    }
+
+    > div {
+      cursor: pointer;
+      &.disable {
+        color: @disabled-color;
+      }
+    }
+  }
+
+  .zoom {
+    display: grid;
+    grid-gap: @grid-gap;
+    grid-template-columns: 1fr 1fr;
+
+    .zoom-item {
+      padding: 8px 0;
+      font-size: 24px;
+      text-align: center;
+      background-color: rgba(#000, 0.1);
+      cursor: pointer;
+
+      &:hover {
+        color: #fff;
+        background-color: @primary-color-hover;
+      }
+
+      &:active {
+        color: #fff;
+        background-color: @primary-color-active;
+      }
+    }
+
+    .zoom-in {
+      border-top-left-radius: 4px;
+      border-bottom-left-radius: 4px;
+    }
+
+    .zoom-out {
+      border-top-right-radius: 4px;
+      border-bottom-right-radius: 4px;
+    }
+  }
+}

+ 134 - 0
src/components/Player/mediaTool.tsx

@@ -0,0 +1,134 @@
+import {
+  CaretDownOutlined,
+  CaretLeftOutlined,
+  CaretRightOutlined,
+  CaretUpOutlined,
+  MinusOutlined,
+  PlusOutlined,
+} from '@ant-design/icons';
+import './mediaTool.less';
+import classNames from 'classnames';
+
+interface MediaToolProps {
+  onMouseUp?: (type: string) => void;
+  onMouseDown?: (type: string) => void;
+  className?: string;
+}
+
+export default (props: MediaToolProps) => {
+  return (
+    <div className={classNames('live-player-tools', props.className)}>
+      <div className={'direction'}>
+        <div
+          className={'direction-item up'}
+          onMouseDown={() => {
+            if (props.onMouseDown) {
+              props.onMouseDown('UP');
+            }
+          }}
+          onMouseUp={() => {
+            if (props.onMouseUp) {
+              props.onMouseUp('UP');
+            }
+          }}
+        >
+          <CaretUpOutlined className={'direction-icon'} />
+        </div>
+        <div
+          className={'direction-item right'}
+          onMouseDown={() => {
+            if (props.onMouseDown) {
+              props.onMouseDown('RIGHT');
+            }
+          }}
+          onMouseUp={() => {
+            if (props.onMouseUp) {
+              props.onMouseUp('RIGHT');
+            }
+          }}
+        >
+          <CaretRightOutlined className={'direction-icon'} />
+        </div>
+        <div
+          className={'direction-item left'}
+          onMouseDown={() => {
+            if (props.onMouseDown) {
+              props.onMouseDown('LEFT');
+            }
+          }}
+          onMouseUp={() => {
+            if (props.onMouseUp) {
+              props.onMouseUp('LEFT');
+            }
+          }}
+        >
+          <CaretLeftOutlined className={'direction-icon'} />
+        </div>
+        <div
+          className={'direction-item down'}
+          onMouseDown={() => {
+            if (props.onMouseDown) {
+              props.onMouseDown('DOWN');
+            }
+          }}
+          onMouseUp={() => {
+            if (props.onMouseUp) {
+              props.onMouseUp('DOWN');
+            }
+          }}
+        >
+          <CaretDownOutlined className={'direction-icon'} />
+        </div>
+        <div
+          className={'direction-audio'}
+          // onMouseDown={() => {
+          //   const { id, channelId } = players[playerActive];
+          //   if (props.onMouseDown && id && channelId) {
+          //     props.onMouseDown(id, channelId, 'AUDIO');
+          //   }
+          // }}
+          // onMouseUp={() => {
+          //   const { id, channelId } = players[playerActive];
+          //   if (props.onMouseUp && id && channelId) {
+          //     props.onMouseUp(id, channelId, 'AUDIO');
+          //   }
+          // }}
+        >
+          {/*<AudioOutlined />*/}
+        </div>
+      </div>
+      <div className={'zoom'}>
+        <div
+          className={'zoom-item zoom-in'}
+          onMouseDown={() => {
+            if (props.onMouseDown) {
+              props.onMouseDown('ZOOM_IN');
+            }
+          }}
+          onMouseUp={() => {
+            if (props.onMouseUp) {
+              props.onMouseUp('ZOOM_IN');
+            }
+          }}
+        >
+          <PlusOutlined />
+        </div>
+        <div
+          className={'zoom-item zoom-out'}
+          onMouseDown={() => {
+            if (props.onMouseDown) {
+              props.onMouseDown('ZOOM_OUT');
+            }
+          }}
+          onMouseUp={() => {
+            if (props.onMouseUp) {
+              props.onMouseUp('ZOOM_OUT');
+            }
+          }}
+        >
+          <MinusOutlined />
+        </div>
+      </div>
+    </div>
+  );
+};

+ 3 - 3
src/components/ProTableCard/CardItems/noticeConfig.tsx

@@ -1,8 +1,8 @@
 import React from 'react';
-import {TableCard} from '@/components';
+import { TableCard } from '@/components';
 import '@/style/common.less';
 import '../index.less';
-import {imgMap, typeList} from './noticeTemplate';
+import { imgMap, typeList } from './noticeTemplate';
 
 export interface NoticeCardProps extends ConfigItem {
   detail?: React.ReactNode;
@@ -15,7 +15,7 @@ export default (props: NoticeCardProps) => {
     <TableCard actions={props.actions} showStatus={false} showMask={false}>
       <div className={'pro-table-card-item'}>
         <div className={'card-item-avatar'}>
-          <img width={88} height={88} src={imgMap[props.type]} alt={props.type}/>
+          <img width={88} height={88} src={imgMap[props.type]} alt={props.type} />
         </div>
         <div className={'card-item-body'}>
           <div className={'card-item-header'}>

+ 34 - 0
src/pages/media/Device/Channel/Live/index.less

@@ -1,4 +1,6 @@
 .media-live {
+  display: flex;
+
   .live-player-tools {
     flex-basis: 230px;
 
@@ -10,6 +12,38 @@
       font-size: 20px !important;
     }
   }
+
+  .media-live-video {
+    position: relative;
+    flex-grow: 1;
+    width: 0;
+
+    .media-tool {
+      position: absolute;
+      top: 4px;
+      right: 0;
+      z-index: 2;
+      display: flex;
+
+      .tool-item {
+        margin-right: 6px;
+        padding: 4px;
+        color: #fff;
+        font-size: 14px;
+        background-color: hsla(0, 0%, 50.2%, 0.8);
+        border-radius: 2px;
+        cursor: pointer;
+
+        &:hover {
+          background-color: hsla(0, 0%, 50.2%, 0.85);
+        }
+
+        &:active {
+          background-color: hsla(0, 0%, 50.2%, 0.9);
+        }
+      }
+    }
+  }
 }
 
 .media-live-tool {

+ 56 - 21
src/pages/media/Device/Channel/Live/index.tsx

@@ -1,7 +1,8 @@
 // 通道直播
-import { useCallback, useRef, useState } from 'react';
+import { useCallback, useEffect, useState } from 'react';
 import { Radio, Modal } from 'antd';
-import { ScreenPlayer } from '@/components';
+import LivePlayer from '@/components/Player';
+import MediaTool from '@/components/Player/mediaTool';
 import { service } from '../index';
 import './index.less';
 
@@ -14,16 +15,23 @@ interface LiveProps {
 
 const LiveFC = (props: LiveProps) => {
   const [mediaType, setMediaType] = useState('mp4');
-  const player = useRef<any>(null);
+  const [url, setUrl] = useState('');
+  const [isRecord, setIsRecord] = useState(false);
 
   const mediaStart = useCallback(
     async (type) => {
       const _url = service.ptzStart(props.deviceId, props.channelId, type);
-      player.current?.replaceVideo(props.deviceId, props.channelId, _url);
+      setUrl(_url);
     },
     [props.channelId, props.deviceId],
   );
 
+  useEffect(() => {
+    if (props.visible) {
+      mediaStart('mp4');
+    }
+  }, [props.visible]);
+
   return (
     <Modal
       destroyOnClose
@@ -43,23 +51,50 @@ const LiveFC = (props: LiveProps) => {
       }}
     >
       <div className={'media-live'}>
-        {props.visible && (
-          <ScreenPlayer
-            id={props.deviceId}
-            channelId={props.channelId}
-            ref={(ref) => {
-              player.current = ref;
-              mediaStart('mp4');
-            }}
-            showScreen={false}
-            onMouseUp={(id, cId) => {
-              service.ptzStop(id, cId);
-            }}
-            onMouseDown={(id, cId, type) => {
-              service.ptzTool(id, cId, type);
-            }}
-          />
-        )}
+        <div className={'media-live-video'}>
+          <div className={'media-tool'}>
+            <div
+              className={'tool-item'}
+              onClick={async () => {
+                if (isRecord) {
+                  const resp = await service.recordStop(props.deviceId, props.channelId, {
+                    local: false,
+                  });
+                  if (resp.status === 200) {
+                    setIsRecord(!isRecord);
+                  }
+                } else {
+                  const resp = await service.recordStart(props.deviceId, props.channelId, {
+                    local: false,
+                  });
+                  if (resp.status === 200) {
+                    setIsRecord(!isRecord);
+                  }
+                }
+              }}
+            >
+              {isRecord ? '停止录像' : '开始录像'}
+            </div>
+            <div className={'tool-item'}>刷新</div>
+            <div
+              className={'tool-item'}
+              onClick={() => {
+                service.mediaStop(props.deviceId, props.channelId);
+              }}
+            >
+              重置
+            </div>
+          </div>
+          <LivePlayer url={url} />
+        </div>
+        <MediaTool
+          onMouseUp={() => {
+            service.ptzStop(props.deviceId, props.channelId);
+          }}
+          onMouseDown={(type) => {
+            service.ptzTool(props.deviceId, props.channelId, type);
+          }}
+        />
       </div>
       <div className={'media-live-tool'}>
         <Radio.Group

+ 1 - 1
src/pages/media/Device/Channel/Tree/index.less

@@ -10,6 +10,6 @@
   }
 
   .channel-tree-content {
-    min-height: 100%;
+    min-height: calc(100% - 50px);
   }
 }

+ 12 - 1
src/pages/media/Device/Channel/Tree/index.tsx

@@ -4,6 +4,7 @@ import { service } from '../index';
 import React, { useEffect, useState } from 'react';
 import './index.less';
 import { SearchOutlined } from '@ant-design/icons';
+import { debounce } from 'lodash';
 
 interface TreeProps {
   deviceId: string;
@@ -41,6 +42,12 @@ export default (props: TreeProps) => {
     }
   };
 
+  const queryTree = (e: any) => {
+    getTreeData(props.deviceId, {
+      terms: [{ column: 'name', termType: 'like', value: `%${e.target.value}%` }],
+    });
+  };
+
   useEffect(() => {
     if (props.deviceId) {
       getDeviceDetail(props.deviceId);
@@ -50,7 +57,11 @@ export default (props: TreeProps) => {
   return (
     <div className={'channel-tree'}>
       <div className={'channel-tree-query'}>
-        <Input placeholder={'请输入目录名称'} suffix={<SearchOutlined />} />
+        <Input
+          placeholder={'请输入目录名称'}
+          suffix={<SearchOutlined />}
+          onChange={debounce(queryTree, 300)}
+        />
       </div>
       <div className={'channel-tree-content'}>
         <Tree

+ 4 - 0
src/pages/media/Device/Channel/service.ts

@@ -36,6 +36,10 @@ class Service extends BaseService<ChannelItem> {
       method: 'POST',
     });
 
+  // 重置
+  mediaStop = (deviceId: string, channelId: string) =>
+    request(`${this.uri}/device/${deviceId}/${channelId}/_stop`, { method: 'POST' });
+
   // 查询是否正在录像
   ptzIsRecord = (deviceId: string, channelId: string) =>
     request(`${this.uri}/device/${deviceId}/${channelId}/live/recording`, { method: 'GET' });

+ 87 - 32
src/pages/media/Device/Playback/index.tsx

@@ -1,7 +1,7 @@
 // 回放
 import { PageContainer } from '@ant-design/pro-layout';
 import LivePlayer from '@/components/Player';
-import { useCallback, useEffect, useState } from 'react';
+import { useEffect, useState } from 'react';
 import { Select, Calendar, Empty, List } from 'antd';
 import { useLocation } from 'umi';
 import Service from './service';
@@ -9,48 +9,63 @@ import './index.less';
 import { recordsItemType } from '@/pages/media/Device/Playback/typings';
 import * as moment from 'moment';
 import classNames from 'classnames';
+import { CloudDownloadOutlined, PauseCircleOutlined, PlayCircleOutlined } from '@ant-design/icons';
 
 const service = new Service('media');
 
 export default () => {
-  const [url] = useState('');
+  const [url, setUrl] = useState('');
   const [type, setType] = useState('local');
   const [historyList, setHistoryList] = useState<recordsItemType[]>([]);
   const [time, setTime] = useState<any>('');
+  const [playTime, setPlayTime] = useState(0);
   const location = useLocation();
 
   const param = new URLSearchParams(location.search);
   const deviceId = param.get('id');
   const channelId = param.get('channelId');
 
-  const queryLocalRecords = useCallback(
-    async (date: any) => {
-      if (deviceId && channelId && date) {
-        const params = {
-          startTime: date.format('YYYY-MM-DD 00:00:00'),
-          endTime: date.format('YYYY-MM-DD 23:59:59'),
-        };
-        let list: recordsItemType[] = [];
-        const localResp = await service.queryRecordLocal(deviceId, channelId, params);
+  const queryLocalRecords = async (date: any) => {
+    if (deviceId && channelId && date) {
+      const params = {
+        startTime: date.format('YYYY-MM-DD 00:00:00'),
+        endTime: date.format('YYYY-MM-DD 23:59:59'),
+      };
+      let list: recordsItemType[] = [];
+      const localResp = await service.queryRecordLocal(deviceId, channelId, params);
 
-        if (localResp.status === 200) {
-          list = [localResp.result];
-        }
+      if (localResp.status === 200) {
+        list = localResp.result;
+      }
+
+      const serviceResp = await service.recordsInServer(deviceId, channelId, {
+        ...params,
+        includeFiles: false,
+      });
+
+      if (serviceResp.status === 200) {
+        list = [...list, ...serviceResp.result];
+      }
 
-        const serviceResp = await service.recordsInServer(deviceId, channelId, {
-          ...params,
-          includeFiles: false,
-        });
+      setHistoryList(list);
+    }
+  };
 
-        if (serviceResp.status === 200) {
-          list = [...list, ...serviceResp.result];
-        }
+  const queryServiceRecords = async (date: any) => {
+    if (deviceId && channelId && date) {
+      const params = {
+        startTime: date.format('YYYY-MM-DD 00:00:00'),
+        endTime: date.format('YYYY-MM-DD 23:59:59'),
+        includeFiles: true,
+      };
 
-        setHistoryList(list);
+      const resp = await service.recordsInServerFiles(deviceId, channelId, params);
+
+      if (resp.status === 200) {
+        setHistoryList(resp.result);
       }
-    },
-    [time],
-  );
+    }
+  };
 
   useEffect(() => {
     setTime(moment(new Date()));
@@ -73,6 +88,11 @@ export default () => {
             style={{ width: '100%' }}
             onSelect={(key: string) => {
               setType(key);
+              if (key === 'cloud') {
+                queryServiceRecords(time);
+              } else {
+                queryLocalRecords(time);
+              }
             }}
           />
           <div className={'playback-calendar'}>
@@ -80,13 +100,17 @@ export default () => {
               value={time}
               onChange={(date) => {
                 setTime(date);
-                queryLocalRecords(date);
+                if (type === 'cloud') {
+                  queryServiceRecords(date);
+                } else {
+                  queryLocalRecords(date);
+                }
               }}
               disabledDate={(currentDate) => currentDate > moment(new Date())}
               fullscreen={false}
             />
           </div>
-          <div className={classNames('playback-list', { 'no-list': !!historyList.length })}>
+          <div className={classNames('playback-list', { 'no-list': !historyList.length })}>
             {historyList && historyList.length ? (
               <List
                 itemLayout="horizontal"
@@ -95,20 +119,51 @@ export default () => {
                   const startTime = moment(item.startTime);
                   const startH = startTime.hours();
                   const startM = startTime.minutes();
-                  const srattD = startTime.date();
+                  const startS = startTime.seconds();
 
                   const endTime = moment(item.endTime);
                   const endH = endTime.hours();
                   const endM = endTime.minutes();
-                  const endD = endTime.date();
+                  const endS = endTime.seconds();
+
                   return (
                     <List.Item
                       actions={[
-                        <a key="list-loadmore-edit">edit</a>,
-                        <a key="list-loadmore-more">more</a>,
+                        <a
+                          key="list-loadmore-edit"
+                          onClick={() => {
+                            if (!playTime) {
+                              setPlayTime(item.startTime);
+                              if (deviceId && channelId) {
+                                setUrl(
+                                  service.playbackLocal(
+                                    deviceId,
+                                    channelId,
+                                    'mp4',
+                                    item.startTime,
+                                    item.endTime,
+                                  ),
+                                );
+                              }
+                            } else {
+                              setPlayTime(0);
+                            }
+                          }}
+                        >
+                          {item.startTime === playTime ? (
+                            <PauseCircleOutlined />
+                          ) : (
+                            <PlayCircleOutlined />
+                          )}
+                        </a>,
+                        <a key="list-loadmore-more">
+                          <CloudDownloadOutlined />
+                        </a>,
                       ]}
                     >
-                      {`${startH}-${startM}-${srattD}`} ~ {`${endH}-${endM}-${endD}`}
+                      <div style={{ textAlign: 'center', paddingLeft: 10 }}>
+                        {`${startH}:${startM}:${startS}`} ~ {`${endH}:${endM}:${endS}`}
+                      </div>
                     </List.Item>
                   );
                 }}

+ 13 - 3
src/pages/media/Device/Playback/service.ts

@@ -16,8 +16,17 @@ class Service extends BaseService<recordsItemType> {
     });
 
   // 播放本地回放
-  playbackLocal = (deviceId: string, channelId: string, suffix: string) =>
-    request(`${this.uri}/device/${deviceId}/${channelId}/playback.${suffix}`, { method: 'GET' });
+  playbackLocal = (
+    deviceId: string,
+    channelId: string,
+    suffix: string,
+    startTime: number,
+    endTime: number,
+    speed: number = 1,
+  ) =>
+    `${
+      this.uri
+    }/device/${deviceId}/${channelId}/playback.${suffix}?:X_Access_Token=${Token.get()}&startTime=${startTime}&endTime=${endTime}&speed=${speed}`;
 
   // 本地录像播放控制
   playbackControl = (deviceId: string, channelId: string) =>
@@ -31,9 +40,10 @@ class Service extends BaseService<recordsItemType> {
     });
 
   // 查询云端回放文件信息
-  recordsInServerFiles = (deviceId: string, channelId: string) =>
+  recordsInServerFiles = (deviceId: string, channelId: string, data: any) =>
     request(`${this.uri}/device/${deviceId}/${channelId}/records/in-server/files`, {
       method: 'POST',
+      data,
     });
 
   // 播放云端回放

+ 20 - 20
src/pages/notice/Config/Detail/index.tsx

@@ -1,9 +1,9 @@
-import {PageContainer} from '@ant-design/pro-layout';
-import {createForm, onFieldValueChange} from '@formily/core';
-import {Card, Col, Input, message, Row} from 'antd';
-import {ISchema} from '@formily/json-schema';
-import {useEffect, useMemo, useState} from 'react';
-import {createSchemaField, observer} from '@formily/react';
+import { PageContainer } from '@ant-design/pro-layout';
+import { createForm, onFieldValueChange } from '@formily/core';
+import { Card, Col, Input, message, Row } from 'antd';
+import { ISchema } from '@formily/json-schema';
+import { useEffect, useMemo, useState } from 'react';
+import { createSchemaField, observer } from '@formily/react';
 import {
   ArrayTable,
   Checkbox,
@@ -20,10 +20,10 @@ import {
   Switch,
 } from '@formily/antd';
 import styles from './index.less';
-import {service, state} from '@/pages/notice/Config';
-import {useAsyncDataSource} from '@/utils/util';
-import {useParams} from 'umi';
-import {typeList} from '@/pages/notice';
+import { service, state } from '@/pages/notice/Config';
+import { useAsyncDataSource } from '@/utils/util';
+import { useParams } from 'umi';
+import { typeList } from '@/pages/notice';
 import FUpload from '@/components/Upload';
 import WeixinCorp from '@/pages/notice/Config/Detail/doc/WeixinCorp';
 import WeixinApp from '@/pages/notice/Config/Detail/doc/WeixinApp';
@@ -35,26 +35,26 @@ import Email from '@/pages/notice/Config/Detail/doc/Email';
 
 export const docMap = {
   weixin: {
-    corpMessage: <WeixinCorp/>,
-    officialMessage: <WeixinApp/>,
+    corpMessage: <WeixinCorp />,
+    officialMessage: <WeixinApp />,
   },
   dingTalk: {
-    dingTalkMessage: <DingTalk/>,
-    dingTalkRobotWebHook: <DingTalkRebot/>,
+    dingTalkMessage: <DingTalk />,
+    dingTalkRobotWebHook: <DingTalkRebot />,
   },
   voice: {
-    aliyun: <AliyunVoice/>,
+    aliyun: <AliyunVoice />,
   },
   sms: {
-    aliyunSms: <AliyunSms/>,
+    aliyunSms: <AliyunSms />,
   },
   email: {
-    embedded: <Email/>,
+    embedded: <Email />,
   },
 };
 
 const Detail = observer(() => {
-  const {id} = useParams<{ id: string }>();
+  const { id } = useParams<{ id: string }>();
 
   const [provider, setProvider] = useState<string>();
   const form = useMemo(
@@ -299,7 +299,7 @@ const Detail = observer(() => {
                     type: 'boolean',
                     'x-component': 'Checkbox.Group',
                     'x-decorator': 'FormItem',
-                    enum: [{label: '开启SSL', value: true}],
+                    enum: [{ label: '开启SSL', value: true }],
                   },
                 },
               },
@@ -354,7 +354,7 @@ const Detail = observer(() => {
         <Row>
           <Col span={10}>
             <Form className={styles.form} form={form} layout={'vertical'}>
-              <SchemaField scope={{useAsyncDataSource, getTypes}} schema={schema}/>
+              <SchemaField scope={{ useAsyncDataSource, getTypes }} schema={schema} />
               <FormButtonGroup.Sticky>
                 <FormButtonGroup.FormItem>
                   <Submit onSubmit={handleSave}>保存</Submit>