Selaa lähdekoodia

feat(merge): merge xyh

Next xyh
Lind 3 vuotta sitten
vanhempi
commit
efee2b6fe7

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

@@ -34,6 +34,7 @@ interface ScreenProps {
    * @param type 当前操作动作
    */
   onMouseUp?: (id: string, channelId: string, type: string) => void;
+  showScreen?: boolean;
 }
 
 export default (props: ScreenProps) => {
@@ -80,25 +81,27 @@ export default (props: ScreenProps) => {
     <div className={'live-player-warp'}>
       <div className={'live-player-content'}>
         <div className={'player-screen-tool'}>
-          <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'}
-          />
+          {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 className={'player-body'}>
           <div className={classNames('player-screen', screenClass)} ref={fullscreenRef}>
@@ -107,7 +110,7 @@ export default (props: ScreenProps) => {
                 <div
                   key={`player_body_${index}`}
                   className={classNames({
-                    active: playerActive === index && !isFullscreen,
+                    active: props.showScreen !== false && playerActive === index && !isFullscreen,
                     'full-screen': isFullscreen,
                   })}
                   onClick={() => {

+ 1 - 0
src/components/ProTableCard/CardItems/mediaDevice.tsx

@@ -14,6 +14,7 @@ const defaultImage = require('/public/images/device-media.png');
 export default (props: ProductCardProps) => {
   return (
     <TableCard
+      showMask={false}
       detail={props.detail}
       actions={props.actions}
       status={props.state.value}

+ 4 - 0
src/pages/device/Product/index.tsx

@@ -223,6 +223,10 @@ const Product = observer(() => {
       dataIndex: 'name',
     },
     {
+      title: '接入方式',
+      dataIndex: 'transportProtocol',
+    },
+    {
       title: '设备类型',
       dataIndex: 'deviceType',
       valueType: 'select',

+ 50 - 0
src/pages/media/Device/Channel/Live.tsx

@@ -0,0 +1,50 @@
+// 通道直播
+import { useState } from 'react';
+import { Radio, Modal } from 'antd';
+import { ScreenPlayer } from '@/components';
+
+interface LiveProps {
+  visible: boolean;
+  url: string;
+  onCancel: () => void;
+}
+
+const LiveFC = (props: LiveProps) => {
+  const [] = useState();
+
+  return (
+    <Modal
+      maskClosable={false}
+      visible={props.visible}
+      title={'播放'}
+      width={800}
+      onCancel={() => {
+        if (props.onCancel) {
+          props.onCancel();
+        }
+      }}
+      onOk={() => {
+        if (props.onCancel) {
+          props.onCancel();
+        }
+      }}
+    >
+      <div>
+        <ScreenPlayer showScreen={false} />
+      </div>
+      <div>
+        <Radio.Group
+          optionType={'button'}
+          buttonStyle={'solid'}
+          options={[
+            { label: 'MP4', value: 'mp4' },
+            { label: 'FLV', value: 'flv' },
+            { label: 'HLS', value: 'hls' },
+          ]}
+        />
+      </div>
+    </Modal>
+  );
+};
+
+export default LiveFC;

+ 225 - 0
src/pages/media/Device/Channel/Save.tsx

@@ -0,0 +1,225 @@
+// Modal 弹窗,用于新增、修改数据
+import { createForm } from '@formily/core';
+import { createSchemaField } from '@formily/react';
+import { Form, FormItem, FormTab, Input, Select, FormGrid } from '@formily/antd';
+import { message, Modal } from 'antd';
+import { useIntl } from '@@/plugin-locale/localeExports';
+import type { ISchema } from '@formily/json-schema';
+import { useEffect, useState } from 'react';
+import { ProviderValue } from '../index';
+
+interface SaveModalProps {
+  visible: boolean;
+  type?: string;
+  model: 'edit' | 'add';
+  deviceId: string;
+  data?: any;
+  onCancel?: () => void;
+  onReload?: () => void;
+  service: any;
+}
+
+const Save = (props: SaveModalProps) => {
+  const { data, onCancel, service } = props;
+  const [loading, setLoading] = useState(false);
+  const intl = useIntl();
+
+  const SchemaField = createSchemaField({
+    components: {
+      FormItem,
+      FormTab,
+      Input,
+      Select,
+      FormGrid,
+    },
+  });
+
+  const form = createForm({
+    validateFirst: true,
+  });
+
+  useEffect(() => {
+    if (form && props.visible) {
+      if (props.model === 'edit') {
+        form.setValues({
+          channelId: data.channelId,
+          name: data.name,
+          id: data.id,
+          manufacturer: data.manufacturer,
+          address: data.address,
+          ptzType: data.ptzType ? data.ptzType.value : 0,
+          description: data.description,
+          media_url: data.other ? data.other['media_url'] : '',
+        });
+      } else {
+        form.setValues({
+          deviceId: props.deviceId,
+        });
+      }
+    }
+  }, [props.visible]);
+
+  const schema: ISchema = {
+    type: 'object',
+    properties: {
+      channelId: {
+        type: 'string',
+        title: '通道ID',
+        'x-decorator': 'FormItem',
+        'x-component': 'Input',
+        'x-component-props': {
+          disabled: props.model === 'edit',
+        },
+      },
+      name: {
+        type: 'string',
+        title: '通道名称',
+        required: true,
+        'x-decorator': 'FormItem',
+        'x-component': 'Input',
+        'x-validator': [
+          {
+            max: 64,
+            message: '最多可输入64个字符',
+          },
+          {
+            required: true,
+            message: '请输入通道名称',
+          },
+        ],
+      },
+      manufacturer: {
+        type: 'string',
+        title: '厂商',
+        'x-visible': props.type === ProviderValue.GB281,
+        'x-decorator': 'FormItem',
+        'x-component': 'Input',
+        'x-validator': [
+          {
+            max: 64,
+            message: '最多可输入64个字符',
+          },
+        ],
+        'x-decorator-props': {
+          gridSpan: 24,
+        },
+      },
+      media_url: {
+        type: 'string',
+        title: '视频地址',
+        'x-visible': props.type === ProviderValue.FIXED,
+        'x-decorator': 'FormItem',
+        'x-component': 'Input',
+        'x-validator': [
+          {
+            max: 64,
+            message: '最多可输入64个字符',
+          },
+        ],
+      },
+      address: {
+        type: 'string',
+        title: '安装地址',
+        'x-decorator': 'FormItem',
+        'x-component': 'Input',
+        'x-validator': [
+          {
+            max: 64,
+            message: '最多可输入64个字符',
+          },
+        ],
+      },
+      ptzType: {
+        type: 'string',
+        title: '云台类型',
+        'x-decorator': 'FormItem',
+        'x-component': 'Select',
+        'x-visible': props.type === ProviderValue.GB281,
+        'x-validator': [
+          {
+            max: 64,
+            message: '最多可输入64个字符',
+          },
+        ],
+        enum: [
+          { label: '未知', value: 0 },
+          { label: '球体', value: 1 },
+          { label: '半球体', value: 2 },
+          { label: '固定枪机', value: 3 },
+          { label: '遥控枪机', value: 4 },
+        ],
+      },
+      description: {
+        type: 'string',
+        title: '说明',
+        'x-decorator': 'FormItem',
+        'x-component': 'Input.TextArea',
+        'x-component-props': {
+          rows: 4,
+          maxLength: 200,
+          showCount: true,
+        },
+      },
+    },
+  };
+
+  const modalClose = () => {
+    if (onCancel) {
+      form.reset();
+      onCancel();
+    }
+  };
+
+  const saveData = async () => {
+    const formData: any = await form.submit();
+    if (formData) {
+      const { media_url, ...extraFormData } = formData;
+      if (media_url) {
+        extraFormData.other = {
+          media_url,
+        };
+      }
+      setLoading(true);
+      const resp =
+        props.model === 'edit'
+          ? await service.updateChannel(formData.id, formData)
+          : await service.saveChannel(formData);
+      setLoading(false);
+
+      if (resp.status === 200) {
+        message.success('操作成功');
+        modalClose();
+        if (props.onReload) {
+          props.onReload();
+        }
+      } else {
+        message.error('操作失败');
+      }
+    }
+  };
+
+  return (
+    <Modal
+      title={intl.formatMessage({
+        id: `pages.data.option.${props.model}`,
+        defaultMessage: '新增',
+      })}
+      maskClosable={false}
+      visible={props.visible}
+      width={550}
+      onOk={saveData}
+      onCancel={() => {
+        modalClose();
+      }}
+      confirmLoading={loading}
+    >
+      <div>
+        <Form form={form} layout={'vertical'}>
+          <SchemaField schema={schema} />
+        </Form>
+      </div>
+    </Modal>
+  );
+};
+
+export default Save;

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

@@ -0,0 +1,15 @@
+.channel-tree {
+  height: 100%;
+  margin-right: 16px;
+  padding: 20px;
+  background-color: #fff;
+  border-radius: 2px;
+
+  .channel-tree-query {
+    margin-bottom: 16px;
+  }
+
+  .channel-tree-content {
+    min-height: 100%;
+  }
+}

+ 34 - 0
src/pages/media/Device/Channel/Tree/index.tsx

@@ -0,0 +1,34 @@
+import { Tree, Input } from 'antd';
+import { useRequest } from 'umi';
+import { service } from '../index';
+import { useEffect } from 'react';
+import './index.less';
+import { SearchOutlined } from '@ant-design/icons';
+
+interface TreeProps {
+  deviceId: string;
+}
+
+export default (props: TreeProps) => {
+  const { data: TreeData, run: getTreeData } = useRequest(service.queryTree, {
+    manual: true,
+    formatResult: (res) => res.result,
+  });
+
+  useEffect(() => {
+    if (props.deviceId) {
+      getTreeData(props.deviceId, {});
+    }
+  }, [props.deviceId]);
+
+  return (
+    <div className={'channel-tree'}>
+      <div className={'channel-tree-query'}>
+        <Input placeholder={'请输入目录名称'} suffix={<SearchOutlined />} />
+      </div>
+      <div className={'channel-tree-content'}>
+        <Tree height={500} treeData={TreeData} />
+      </div>
+    </div>
+  );
+};

+ 151 - 31
src/pages/media/Device/Channel/index.tsx

@@ -3,23 +3,52 @@ import { PageContainer } from '@ant-design/pro-layout';
 import ProTable, { ActionType, ProColumns } from '@jetlinks/pro-table';
 import SearchComponent from '@/components/SearchComponent';
 import './index.less';
-import { useRef, useState } from 'react';
+import { useEffect, useRef, useState } from 'react';
 import { ChannelItem } from '@/pages/media/Device/Channel/typings';
-import { useIntl } from '@@/plugin-locale/localeExports';
+import { useIntl, useLocation, useHistory } from 'umi';
 import { BadgeStatus } from '@/components';
 import { StatusColorEnum } from '@/components/BadgeStatus';
-import { Button, Tooltip } from 'antd';
-import { EditOutlined, PlusOutlined } from '@ant-design/icons';
+import { Button, message, Popconfirm, Tooltip } from 'antd';
+import {
+  DeleteOutlined,
+  EditOutlined,
+  PlusOutlined,
+  VideoCameraOutlined,
+  VideoCameraAddOutlined,
+} from '@ant-design/icons';
+import Save from './Save';
 import Service from './service';
+import { ProviderValue } from '../index';
+import Live from './Live';
+import { getMenuPathByCode, MENUS_CODE } from '@/utils/menu';
+import Tree from './Tree';
 
-export const service = new Service('media/device');
+export const service = new Service('media');
 
 export default () => {
   const intl = useIntl();
   const actionRef = useRef<ActionType>();
   const [queryParam, setQueryParam] = useState({});
-  const [, setVisible] = useState<boolean>(false);
-  const [, setCurrent] = useState<ChannelItem>();
+  const [visible, setVisible] = useState<boolean>(false);
+  const [liveVisible, setLiveVisible] = useState(false);
+  const [current, setCurrent] = useState<ChannelItem>();
+  const [deviceId, setDeviceId] = useState('');
+  const [type, setType] = useState('');
+
+  const location = useLocation();
+  const history = useHistory();
+
+  useEffect(() => {
+    const param = new URLSearchParams(location.search);
+    const _deviceId = param.get('id');
+    const _type = param.get('type');
+    if (_deviceId) {
+      setDeviceId(_deviceId);
+    }
+    if (_type) {
+      setType(_type);
+    }
+  }, [location]);
 
   /**
    * table 查询参数
@@ -29,10 +58,20 @@ export default () => {
     setQueryParam(data);
   };
 
+  const deleteItem = async (id: string) => {
+    const resp: any = await service.removeChannel(id);
+    if (resp.status === 200) {
+      actionRef.current?.reload();
+    } else {
+      message.error('删除失败');
+    }
+  };
+
   const columns: ProColumns<ChannelItem>[] = [
     {
-      dataIndex: 'id',
-      title: 'ID',
+      dataIndex: 'channelId',
+      title: '通道ID',
+      width: 220,
     },
     {
       dataIndex: 'name',
@@ -40,6 +79,7 @@ export default () => {
         id: 'pages.table.name',
         defaultMessage: '名称',
       }),
+      width: 220,
     },
     {
       dataIndex: 'manufacturer',
@@ -47,6 +87,11 @@ export default () => {
         id: 'pages.media.device.manufacturer',
         defaultMessage: '设备厂家',
       }),
+      hideInTable: type !== ProviderValue.GB281,
+    },
+    {
+      dataIndex: 'address',
+      title: '安装地址',
     },
     {
       dataIndex: 'state',
@@ -56,13 +101,13 @@ export default () => {
       }),
       render: (_, record) => (
         <BadgeStatus
-          status={record.status}
+          status={record.status.value}
           statusNames={{
             online: StatusColorEnum.success,
             offline: StatusColorEnum.error,
             notActive: StatusColorEnum.processing,
           }}
-          text={record.status}
+          text={record.status.text}
         />
       ),
     },
@@ -74,7 +119,7 @@ export default () => {
       valueType: 'option',
       align: 'center',
       width: 200,
-      render: () => [
+      render: (_, record) => [
         <Tooltip
           key="edit"
           title={intl.formatMessage({
@@ -82,10 +127,53 @@ export default () => {
             defaultMessage: '编辑',
           })}
         >
-          <a onClick={() => {}}>
+          <a
+            onClick={() => {
+              setCurrent(record);
+              setVisible(true);
+            }}
+          >
             <EditOutlined />
           </a>
         </Tooltip>,
+        <Tooltip key={'live'} title={'播发'}>
+          <a
+            onClick={() => {
+              setLiveVisible(true);
+            }}
+          >
+            <VideoCameraOutlined />
+          </a>
+        </Tooltip>,
+        <Tooltip key={'playback'} title={'回放'}>
+          <a
+            onClick={() => {
+              history.push(
+                `${getMenuPathByCode(MENUS_CODE['media/Device/Playback'])}?id=${record.channelId}`,
+              );
+            }}
+          >
+            <VideoCameraAddOutlined />
+          </a>
+        </Tooltip>,
+        type === ProviderValue.FIXED ? (
+          <Tooltip key={'updateChannel'} title="删除">
+            <Popconfirm
+              key="delete"
+              title={intl.formatMessage({
+                id: 'page.table.isDelete',
+                defaultMessage: '是否删除?',
+              })}
+              onConfirm={async () => {
+                deleteItem(record.id);
+              }}
+            >
+              <Button type={'link'} style={{ padding: '4px' }}>
+                <DeleteOutlined />
+              </Button>
+            </Popconfirm>
+          </Tooltip>
+        ) : null,
       ],
     },
   ];
@@ -93,7 +181,9 @@ export default () => {
   return (
     <PageContainer>
       <div className={'device-channel-warp'}>
-        <div className={'left'}></div>
+        <div className={'left'}>
+          <Tree deviceId={deviceId} />
+        </div>
         <div className={'right'}>
           <SearchComponent field={columns} onSearch={searchFn} />
           <ProTable<ChannelItem>
@@ -101,8 +191,13 @@ export default () => {
             actionRef={actionRef}
             options={{ fullScreen: true }}
             params={queryParam}
+            defaultParams={[
+              {
+                column: 'id',
+              },
+            ]}
             request={(params = {}) =>
-              service.query({
+              service.queryChannel(deviceId, {
                 ...params,
                 sorts: [
                   {
@@ -114,25 +209,50 @@ export default () => {
             }
             rowKey="id"
             search={false}
-            headerTitle={[
-              <Button
-                onClick={() => {
-                  setCurrent(undefined);
-                  setVisible(true);
-                }}
-                key="button"
-                icon={<PlusOutlined />}
-                type="primary"
-              >
-                {intl.formatMessage({
-                  id: 'pages.data.option.add',
-                  defaultMessage: '新增',
-                })}
-              </Button>,
-            ]}
+            headerTitle={
+              type === ProviderValue.FIXED
+                ? [
+                    <Button
+                      onClick={() => {
+                        setCurrent(undefined);
+                        setVisible(true);
+                      }}
+                      key="button"
+                      icon={<PlusOutlined />}
+                      type="primary"
+                    >
+                      {intl.formatMessage({
+                        id: 'pages.data.option.add',
+                        defaultMessage: '新增',
+                      })}
+                    </Button>,
+                  ]
+                : null
+            }
           />
         </div>
       </div>
+      <Save
+        visible={visible}
+        service={service}
+        model={current ? 'edit' : 'add'}
+        type={type}
+        data={current}
+        deviceId={deviceId}
+        onCancel={() => {
+          setVisible(false);
+        }}
+        onReload={() => {
+          actionRef.current?.reload();
+        }}
+      />
+      <Live
+        visible={liveVisible}
+        url={''}
+        onCancel={() => {
+          setLiveVisible(false);
+        }}
+      />
     </PageContainer>
   );
 };

+ 13 - 2
src/pages/media/Device/Channel/service.ts

@@ -4,8 +4,19 @@ import type { ChannelItem } from './typings';
 
 class Service extends BaseService<ChannelItem> {
   //
-  queryTree = (id: string, data: any) =>
-    request(`${this.uri}/${id}/catalog/_query/tree`, { method: 'PATCH', data });
+  queryTree = (id: string, data?: any) =>
+    request(`${this.uri}/device/${id}/catalog/_query/tree`, { method: 'POST', data });
+
+  // 查询设备通道列表
+  queryChannel = (id: string, data: any) =>
+    request(`${this.uri}/device/${id}/channel/_query`, { method: 'POST', data });
+
+  updateChannel = (id: string, data: any) =>
+    request(`${this.uri}/channel/${id}`, { method: 'PUT', data });
+
+  saveChannel = (data: any) => request(`${this.uri}/channel`, { method: 'POST', data });
+
+  removeChannel = (id: string) => request(`${this.uri}/channel/${id}`, { method: 'DELETE' });
 }
 
 export default Service;

+ 4 - 1
src/pages/media/Device/Channel/typings.d.ts

@@ -91,7 +91,10 @@ export interface ChannelItem {
   model: string;
   address: string;
   provider: string;
-  status: ChannelStatusType;
+  status: {
+    value: string;
+    text: string;
+  };
   others: object;
   description: string;
   parentChannelId: string;

+ 6 - 0
src/pages/media/Device/Playback/index.tsx

@@ -0,0 +1,6 @@
+// 回放
+import { PageContainer } from '@ant-design/pro-layout';
+
+export default () => {
+  return <PageContainer>回放</PageContainer>;
+};

+ 14 - 16
src/pages/media/Device/index.tsx

@@ -6,7 +6,6 @@ import { Button, message, Popconfirm, Tooltip } from 'antd';
 import {
   DeleteOutlined,
   EditOutlined,
-  EyeOutlined,
   PlusOutlined,
   SyncOutlined,
   PartitionOutlined,
@@ -17,7 +16,7 @@ import { BadgeStatus, ProTableCard } from '@/components';
 import { StatusColorEnum } from '@/components/BadgeStatus';
 import SearchComponent from '@/components/SearchComponent';
 import MediaDevice from '@/components/ProTableCard/CardItems/mediaDevice';
-import { getMenuPathByParams, MENUS_CODE } from '@/utils/menu';
+import { getMenuPathByCode, MENUS_CODE } from '@/utils/menu';
 import Service from './service';
 import Save from './Save';
 
@@ -28,6 +27,11 @@ const providerType = {
   'fixed-media': '固定地址',
 };
 
+export const ProviderValue = {
+  GB281: 'gb28181-2016',
+  FIXED: 'fixed-media',
+};
+
 const Device = () => {
   const intl = useIntl();
   const actionRef = useRef<ActionType>();
@@ -163,7 +167,11 @@ const Device = () => {
         <Tooltip key={'viewChannel'} title="查看通道">
           <a
             onClick={() => {
-              history.push(`${getMenuPathByParams(MENUS_CODE['media/Device/Channel'], record.id)}`);
+              history.push(
+                `${getMenuPathByCode(MENUS_CODE['media/Device/Channel'])}?id=${record.id}&type=${
+                  record.provider
+                }`,
+              );
             }}
           >
             <PartitionOutlined />
@@ -252,18 +260,6 @@ const Device = () => {
         cardRender={(record) => (
           <MediaDevice
             {...record}
-            detail={
-              <div
-                style={{ fontSize: 18, padding: 8 }}
-                onClick={() => {
-                  history.push(
-                    `${getMenuPathByParams(MENUS_CODE['device/Product/Detail'], record.id)}`,
-                  );
-                }}
-              >
-                <EyeOutlined />
-              </div>
-            }
             actions={[
               <Button
                 key="edit"
@@ -284,7 +280,9 @@ const Device = () => {
                 key={'viewChannel'}
                 onClick={() => {
                   history.push(
-                    `${getMenuPathByParams(MENUS_CODE['media/Device/Channel'], record.id)}`,
+                    `${getMenuPathByCode(MENUS_CODE['media/Device/Channel'])}?id=${
+                      record.id
+                    }&type=${record.provider}`,
                   );
                 }}
               >

+ 1 - 0
src/utils/menu/router.ts

@@ -46,6 +46,7 @@ export const MENUS_CODE = {
   'media/Config': 'media/Config',
   'media/Device': 'media/Device',
   'media/Device/Channel': 'media/Device/Channel',
+  'media/Device/Playback': 'media/Device/Playback',
   'media/Reveal': 'media/Reveal',
   'media/Stream': 'media/Stream',
   'media/Stream/Detail': 'media/Stream/Detail',