Sfoglia il codice sorgente

fix(merge): merge sc

lind 3 anni fa
parent
commit
545e6e61b4

+ 149 - 20
src/components/Player/ScreenPlayer.tsx

@@ -1,7 +1,7 @@
 import { useCallback, useEffect, useRef, useState } from 'react';
 import classNames from 'classnames';
 import LivePlayer from './index';
-import { Radio } from 'antd';
+import { Button, Dropdown, Empty, Input, Menu, Popover, Radio, Tooltip } from 'antd';
 import { useFullscreen } from 'ahooks';
 import './index.less';
 import {
@@ -9,9 +9,12 @@ import {
   CaretLeftOutlined,
   CaretRightOutlined,
   CaretUpOutlined,
+  DeleteOutlined,
   MinusOutlined,
   PlusOutlined,
+  QuestionCircleOutlined,
 } from '@ant-design/icons';
+import Service from './service';
 
 type Player = {
   id?: string;
@@ -37,10 +40,18 @@ interface ScreenProps {
   showScreen?: boolean;
 }
 
+const service = new Service();
+
+const DEFAULT_SAVE_CODE = 'screen_save';
+
 export default (props: ScreenProps) => {
   const [screen, setScreen] = useState(1);
   const [players, setPlayers] = useState<Player[]>([]);
   const [playerActive, setPlayerActive] = useState(0);
+  const [historyList, setHistoryList] = useState<any>([]);
+  const [historyTitle, setHistoryTitle] = useState('');
+  const [visible, setVisible] = useState(false);
+
   const fullscreenRef = useRef(null);
   const [isFullscreen, { setFull }] = useFullscreen(fullscreenRef);
 
@@ -62,6 +73,42 @@ export default (props: ScreenProps) => {
     [players, playerActive, screen],
   );
 
+  const handleHistory = (item: any) => {
+    const log = JSON.parse(item.content || '{}');
+    setScreen(log.screen);
+    setPlayers(log.players);
+  };
+
+  const getHistory = async () => {
+    const resp = await service.history.query(DEFAULT_SAVE_CODE);
+    if (resp.status === 200) {
+      setHistoryList(resp.result);
+    }
+  };
+
+  const deleteHistory = async (id: string) => {
+    const resp = await service.history.remove(DEFAULT_SAVE_CODE, id);
+    if (resp.status === 200) {
+      getHistory();
+      setVisible(false);
+    }
+  };
+
+  const saveHistory = useCallback(async () => {
+    const param = {
+      name: historyTitle,
+      content: JSON.stringify({
+        screen: screen,
+        players: players,
+      }),
+    };
+    const resp = await service.history.save(DEFAULT_SAVE_CODE, param);
+    if (resp.status === 200) {
+      setVisible(false);
+      getHistory();
+    }
+  }, [players, historyTitle, screen]);
+
   useEffect(() => {
     const arr = new Array(screen).fill(1).map(() => ({ id: '', url: '' }));
     setPlayers(arr);
@@ -75,32 +122,114 @@ export default (props: ScreenProps) => {
     }
   }, [props.url]);
 
+  useEffect(() => {
+    if (props.showScreen !== false) {
+      getHistory();
+    }
+  }, []);
+
   const screenClass = `screen-${screen}`;
 
+  const DropdownMenu = (
+    <Menu>
+      {historyList.length ? (
+        historyList.map((item: any) => {
+          return (
+            <Menu.Item
+              key={item.id}
+              onClick={() => {
+                handleHistory(item);
+              }}
+            >
+              {item.name}
+              <DeleteOutlined
+                onClick={() => {
+                  deleteHistory(item.id);
+                }}
+              />
+            </Menu.Item>
+          );
+        })
+      ) : (
+        <Empty />
+      )}
+    </Menu>
+  );
+
   return (
     <div className={'live-player-warp'}>
       <div className={'live-player-content'}>
         <div className={'player-screen-tool'}>
           {props.showScreen !== false && (
-            <Radio.Group
-              options={[
-                { label: '单屏', value: 1 },
-                { label: '四分屏', value: 4 },
-                { label: '九分屏', value: 9 },
-                { label: '全屏', value: 0 },
-              ]}
-              value={screen}
-              onChange={(e) => {
-                if (e.target.value) {
-                  setScreen(e.target.value);
-                } else {
-                  // 全屏操作
-                  setFull();
-                }
-              }}
-              optionType={'button'}
-              buttonStyle={'solid'}
-            />
+            <>
+              <div></div>
+              <div>
+                <Radio.Group
+                  options={[
+                    { label: '单屏', value: 1 },
+                    { label: '四分屏', value: 4 },
+                    { label: '九分屏', value: 9 },
+                    { label: '全屏', value: 0 },
+                  ]}
+                  value={screen}
+                  onChange={(e) => {
+                    if (e.target.value) {
+                      setScreen(e.target.value);
+                    } else {
+                      // 全屏操作
+                      setFull();
+                    }
+                  }}
+                  optionType={'button'}
+                  buttonStyle={'solid'}
+                />
+                {/*<Tooltip*/}
+                {/*  title={''}*/}
+                {/*>*/}
+                {/*  <QuestionCircleOutlined />*/}
+                {/*</Tooltip>*/}
+              </div>
+              <div className={'screen-tool-save'}>
+                <Popover
+                  content={
+                    <div style={{ width: 300 }}>
+                      <Input.TextArea
+                        rows={3}
+                        onChange={(e) => {
+                          setHistoryTitle(e.target.value);
+                        }}
+                      />
+                      <Button
+                        type={'primary'}
+                        onClick={saveHistory}
+                        style={{ width: '100%', marginTop: 16 }}
+                      >
+                        保存
+                      </Button>
+                    </div>
+                  }
+                  title="分屏名称"
+                  trigger="click"
+                  visible={visible}
+                  onVisibleChange={(v) => {
+                    setVisible(v);
+                  }}
+                >
+                  <Dropdown.Button
+                    type={'primary'}
+                    overlay={DropdownMenu}
+                    onClick={() => {
+                      setVisible(true);
+                    }}
+                  >
+                    保存
+                  </Dropdown.Button>
+                </Popover>
+                <Tooltip title={'可保存分屏配置记录'}>
+                  <QuestionCircleOutlined style={{ marginLeft: 8 }} />
+                </Tooltip>
+              </div>
+            </>
           )}
         </div>
         <div className={'player-body'}>

+ 3 - 2
src/components/Player/index.less

@@ -12,7 +12,8 @@
 
     .player-screen-tool {
       display: flex;
-      justify-content: center;
+      align-items: center;
+      justify-content: space-between;
       margin-bottom: 20px;
 
       .ant-radio-button-wrapper {
@@ -60,7 +61,7 @@
   }
 
   .live-player-tools {
-    flex-basis: 290px;
+    flex-basis: 288px;
     padding: 50px 12px 0 40px;
 
     .direction {

+ 24 - 0
src/components/Player/service.ts

@@ -0,0 +1,24 @@
+import { request } from 'umi';
+import SystemConst from '@/utils/const';
+
+class Service {
+  private api = `/${SystemConst.API_BASE}/user/settings`;
+
+  public history = {
+    query: (type: string) =>
+      request(`${this.api}/${type}`, {
+        method: 'GET',
+      }),
+    save: (type: string, data: Record<string, unknown>) =>
+      request(`${this.api}/${type}`, {
+        method: 'POST',
+        data,
+      }),
+    remove: (type: string, key: string) =>
+      request(`${this.api}/${type}/${key}`, {
+        method: 'DELETE',
+      }),
+  };
+}
+
+export default Service;

+ 47 - 6
src/pages/media/Device/Channel/Tree/index.tsx

@@ -1,23 +1,48 @@
-import { Tree, Input } from 'antd';
+import { Input, Tree } from 'antd';
 import { useRequest } from 'umi';
 import { service } from '../index';
-import { useEffect } from 'react';
+import React, { useEffect, useState } from 'react';
 import './index.less';
 import { SearchOutlined } from '@ant-design/icons';
 
 interface TreeProps {
   deviceId: string;
+  onSelect: (id: React.Key) => void;
 }
 
 export default (props: TreeProps) => {
-  const { data: TreeData, run: getTreeData } = useRequest(service.queryTree, {
+  const [treeData, setTreeData] = useState<any>([]);
+  const [selectedKeys, setSelectedKeys] = useState<React.Key[]>(['']);
+  const { run: getTreeData } = useRequest(service.queryTree, {
     manual: true,
-    formatResult: (res) => res.result,
+    onSuccess: (res) => {
+      treeData[0].children = res.result;
+      setTreeData(treeData);
+    },
   });
 
+  /**
+   * 获取设备详情
+   * @param id
+   */
+  const getDeviceDetail = async (id: string) => {
+    const deviceResp = await service.deviceDetail(id);
+    if (deviceResp.status === 200) {
+      setTreeData([
+        {
+          id,
+          name: deviceResp.result.name,
+          children: [],
+        },
+      ]);
+      setSelectedKeys([id]);
+      getTreeData(props.deviceId, {});
+    }
+  };
+
   useEffect(() => {
     if (props.deviceId) {
-      getTreeData(props.deviceId, {});
+      getDeviceDetail(props.deviceId);
     }
   }, [props.deviceId]);
 
@@ -27,7 +52,23 @@ export default (props: TreeProps) => {
         <Input placeholder={'请输入目录名称'} suffix={<SearchOutlined />} />
       </div>
       <div className={'channel-tree-content'}>
-        <Tree height={500} treeData={TreeData} />
+        <Tree
+          height={500}
+          selectedKeys={selectedKeys}
+          treeData={treeData}
+          onSelect={(keys) => {
+            if (keys.length) {
+              setSelectedKeys(keys);
+              if (props.onSelect) {
+                props.onSelect(keys[0]);
+              }
+            }
+          }}
+          fieldNames={{
+            key: 'id',
+            title: 'name',
+          }}
+        />
       </div>
     </div>
   );

+ 19 - 3
src/pages/media/Device/Channel/index.tsx

@@ -5,7 +5,7 @@ import SearchComponent from '@/components/SearchComponent';
 import './index.less';
 import { useEffect, useRef, useState } from 'react';
 import { ChannelItem } from '@/pages/media/Device/Channel/typings';
-import { useIntl, useLocation, useHistory } from 'umi';
+import { useHistory, useIntl, useLocation } from 'umi';
 import { BadgeStatus } from '@/components';
 import { StatusColorEnum } from '@/components/BadgeStatus';
 import { Button, message, Popconfirm, Tooltip } from 'antd';
@@ -13,8 +13,8 @@ import {
   DeleteOutlined,
   EditOutlined,
   PlusOutlined,
-  VideoCameraOutlined,
   VideoCameraAddOutlined,
+  VideoCameraOutlined,
 } from '@ant-design/icons';
 import Save from './Save';
 import Service from './service';
@@ -182,7 +182,23 @@ export default () => {
     <PageContainer>
       <div className={'device-channel-warp'}>
         <div className={'left'}>
-          <Tree deviceId={deviceId} />
+          <Tree
+            deviceId={deviceId}
+            onSelect={(key) => {
+              if (key === deviceId && actionRef.current?.reset) {
+                actionRef.current?.reset();
+              } else {
+                setQueryParam({
+                  terms: [
+                    {
+                      column: 'parentId',
+                      value: key,
+                    },
+                  ],
+                });
+              }
+            }}
+          />
         </div>
         <div className={'right'}>
           <SearchComponent field={columns} onSearch={searchFn} />

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

@@ -17,6 +17,9 @@ class Service extends BaseService<ChannelItem> {
   saveChannel = (data: any) => request(`${this.uri}/channel`, { method: 'POST', data });
 
   removeChannel = (id: string) => request(`${this.uri}/channel/${id}`, { method: 'DELETE' });
+
+  // 设备详情
+  deviceDetail = (id: string) => request(`${this.uri}/device/${id}`, { method: 'GET' });
 }
 
 export default Service;

+ 14 - 1
src/pages/media/SplitScreen/index.less

@@ -1,3 +1,4 @@
+@import '~antd/es/style/themes/default.less';
 @padding: 20px;
 
 .split-screen {
@@ -7,6 +8,18 @@
     width: 300px;
     padding-right: @padding;
     border-right: 1px solid #f0f0f0;
+
+    .online {
+      color: @success-color;
+    }
+
+    .offline {
+      color: @disabled-color;
+    }
+
+    .left-search {
+      margin-bottom: 12px;
+    }
   }
 
   .right-content {
@@ -55,7 +68,7 @@
       }
 
       .player-tools {
-        flex-basis: 290px;
+        flex-basis: 280px;
         padding: 50px 12px 0 12px;
       }
     }

+ 16 - 9
src/pages/media/SplitScreen/index.tsx

@@ -1,26 +1,33 @@
 // 视频分屏
 import { PageContainer } from '@ant-design/pro-layout';
-import { Card } from 'antd';
+import { Card, message } from 'antd';
 import LeftTree from './tree';
 import { ScreenPlayer } from '@/components';
-import { ptzStop, ptzTool } from './service';
+import { ptzStart, ptzStop, ptzTool } from './service';
 import { useState } from 'react';
 import './index.less';
 
 const SplitScreen = () => {
   const [deviceId, setDeviceId] = useState('');
-  const [channelId] = useState('');
-  const [url] = useState('');
+  const [channelId, setChannelId] = useState('');
+  const [url, setUrl] = useState('');
+
+  const mediaStart = async (dId: string, cId: string) => {
+    setChannelId(dId);
+    setDeviceId(cId);
+    const resp = await ptzStart(dId, cId);
+    if (resp.status === 200) {
+      setUrl(resp.result.mp4);
+    } else {
+      message.error(resp.result);
+    }
+  };
 
   return (
     <PageContainer>
       <Card>
         <div className="split-screen">
-          <LeftTree
-            onSelect={(id) => {
-              setDeviceId(id);
-            }}
-          />
+          <LeftTree onSelect={mediaStart} />
           <div className="right-content">
             <ScreenPlayer
               id={deviceId}

+ 5 - 1
src/pages/media/SplitScreen/service.ts

@@ -9,7 +9,7 @@ export const getMediaTree = (data?: any) =>
 
 // 开始直播
 export const ptzStart = (deviceId: string, channelId: string) =>
-  request(`${url}/device/${deviceId}/${channelId}/_pzt/_start`, { method: 'POST' });
+  request(`${url}/device/${deviceId}/${channelId}/_start`, { method: 'POST' });
 
 // 云台控制-停止
 export const ptzStop = (deviceId: string, channelId: string) =>
@@ -18,3 +18,7 @@ export const ptzStop = (deviceId: string, channelId: string) =>
 // 云台控制-缩放、转向等
 export const ptzTool = (deviceId: string, channelId: string, direct: string, speed: number = 90) =>
   request(`${url}/device/${deviceId}/${channelId}/_pzt/${direct}/${speed}`, { method: 'POST' });
+
+// 查询设备通道列表
+export const queryChannel = (data: any) =>
+  request(`${url}/channel/_query`, { method: 'POST', data });

+ 141 - 15
src/pages/media/SplitScreen/tree.tsx

@@ -1,36 +1,162 @@
-import { Input, Tree } from 'antd';
-import { getMediaTree } from './service';
-import { useRequest } from 'umi';
-import { useEffect } from 'react';
+import { Tree } from 'antd';
+import { getMediaTree, queryChannel } from './service';
+import React, { useEffect, useState } from 'react';
+import { VideoCameraOutlined } from '@ant-design/icons';
 
 type LeftTreeTYpe = {
-  onSelect?: (deviceId: string) => void;
+  onSelect?: (deviceId: string, channelId: string) => void;
 };
 
+interface DataNode {
+  name: string;
+  id: string;
+  isLeaf?: boolean;
+  channelNumber?: number;
+  icon?: React.ReactNode;
+  status: {
+    text: string;
+    value: string;
+  };
+  children?: DataNode[];
+}
+
 const LeftTree = (props: LeftTreeTYpe) => {
-  const { data, run: queryTree } = useRequest(getMediaTree, {
-    manual: true,
-  });
+  const [treeData, setTreeData] = useState<DataNode[]>([]);
+
+  /**
+   * 是否为子节点
+   * @param node
+   */
+  const isLeaf = (node: DataNode): boolean => {
+    if (node.channelNumber) {
+      return false;
+    }
+    return true;
+  };
+
+  /**
+   * 获取设备列表
+   */
+  const getDeviceList = async () => {
+    const resp = await getMediaTree({ paging: false });
+    if (resp.status === 200) {
+      setTreeData(
+        resp.result.map((item: any) => {
+          const extra: any = {};
+          extra.isLeaf = isLeaf(item);
+
+          return {
+            ...item,
+            ...extra,
+          };
+        }),
+      );
+    }
+  };
+
+  const updateTreeData = (list: DataNode[], key: React.Key, children: DataNode[]): DataNode[] => {
+    return list.map((node) => {
+      if (node.id === key) {
+        return {
+          ...node,
+          children: node.children ? [...node.children, ...children] : children,
+        };
+      }
+
+      if (node.children) {
+        return {
+          ...node,
+          children: updateTreeData(node.children, key, children),
+        };
+      }
+      return node;
+    });
+  };
+
+  /**
+   * 获取子节点
+   * @param key
+   * @param params
+   */
+  const getChildren = (key: React.Key, params: any): Promise<any> => {
+    return new Promise(async (resolve) => {
+      const resp = await queryChannel(params);
+      if (resp.status === 200) {
+        const { total, pageIndex, pageSize } = resp.result;
+        setTreeData((origin) => {
+          const data = updateTreeData(
+            origin,
+            key,
+            resp.result.data.map((item: DataNode) => ({
+              ...item,
+              icon: <VideoCameraOutlined className={item.status.value} />,
+              isLeaf: isLeaf(item),
+            })),
+          );
+
+          if (total > (pageIndex + 1) * pageSize) {
+            setTimeout(() => {
+              getChildren(key, {
+                ...params,
+                pageIndex: params.pageIndex + 1,
+              });
+            }, 50);
+          }
+
+          return data;
+        });
+        resolve(resp.result);
+      }
+    });
+  };
+
+  const onLoadData = ({ key, children }: any): Promise<void> => {
+    return new Promise(async (resolve) => {
+      if (children) {
+        resolve();
+        return;
+      }
+      await getChildren(key, {
+        pageIndex: 0,
+        pageSize: 100,
+        terms: [
+          {
+            column: 'deviceId',
+            value: key,
+          },
+        ],
+      });
+      resolve();
+    });
+  };
 
   useEffect(() => {
-    queryTree({ paging: false });
+    getDeviceList();
   }, []);
 
   return (
     <div className="left-content">
-      <Input.Search />
+      {/*<Input*/}
+      {/*  className={'left-search'}*/}
+      {/*  placeholder={'请输入设备名称'}*/}
+      {/*  suffix={<SearchOutlined />}*/}
+      {/*  onChange={debounce(ThreeNodeSearch, 300)}*/}
+      {/*/>*/}
       <Tree
-        height={500}
+        showIcon
+        showLine={{ showLeafIcon: false }}
+        height={550}
         fieldNames={{
           title: 'name',
           key: 'id',
         }}
-        onSelect={(key) => {
-          if (props.onSelect) {
-            props.onSelect(key[0] as string);
+        onSelect={(_, { node }: any) => {
+          if (props.onSelect && node.isLeaf) {
+            props.onSelect(node.deviceId, node.channelId);
           }
         }}
-        treeData={data}
+        loadData={onLoadData}
+        treeData={treeData}
       />
     </div>
   );

+ 1 - 1
src/pages/system/Menu/Detail/edit.tsx

@@ -41,7 +41,7 @@ export default (props: EditProps) => {
 
   const { data: permissions, run: queryPermissions } = useRequest(service.queryPermission, {
     manual: true,
-    formatResult: (response) => response.result.data,
+    formatResult: (response) => response.result,
   });
 
   const { data: menuThree, run: queryMenuThree } = useRequest(service.queryMenuThree, {

+ 1 - 1
src/pages/system/Menu/service.ts

@@ -21,7 +21,7 @@ class Service extends BaseService<MenuItem> {
    * @param data
    */
   queryPermission = (data: any) =>
-    request(`${SystemConst.API_BASE}/permission/_query`, { method: 'POST', data });
+    request(`${SystemConst.API_BASE}/permission/_query/no-paging`, { method: 'POST', data });
 
   queryDetail = (id: string) => request(`${this.uri}/${id}`, { method: 'GET' });