Procházet zdrojové kódy

feat(视频设备): 新增视频设备列表;新增视频设备编辑

xieyonghong před 3 roky
rodič
revize
265bb7f9da

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

@@ -0,0 +1,56 @@
+import React from 'react';
+import type { DeviceItem } from '@/pages/media/Device/typings';
+import { StatusColorEnum } from '@/components/BadgeStatus';
+import { TableCard } from '@/components';
+import '@/style/common.less';
+import '../index.less';
+
+export interface ProductCardProps extends DeviceItem {
+  detail?: React.ReactNode;
+  actions?: React.ReactNode[];
+}
+const defaultImage = require('/public/images/device-media.png');
+
+export default (props: ProductCardProps) => {
+  return (
+    <TableCard
+      detail={props.detail}
+      actions={props.actions}
+      status={props.state.value}
+      statusText={props.state.text}
+      statusNames={{
+        offline: StatusColorEnum.error,
+        online: StatusColorEnum.processing,
+      }}
+    >
+      <div className={'pro-table-card-item'}>
+        <div className={'card-item-avatar'}>
+          <img width={88} height={88} src={props.photoUrl || defaultImage} alt={''} />
+        </div>
+        <div className={'card-item-body'}>
+          <div className={'card-item-header'}>
+            <span className={'card-item-header-name ellipsis'}>{props.name}</span>
+          </div>
+          <div className={'card-item-content'}>
+            <div>
+              <label>厂商</label>
+              <div className={'ellipsis'}>{props.manufacturer || '--'}</div>
+            </div>
+            <div>
+              <label>通道数量</label>
+              <div className={'ellipsis'}>{props.channelNumber || '--'}</div>
+            </div>
+            <div>
+              <label>型号</label>
+              <div className={'ellipsis'}>{props.model || '--'}</div>
+            </div>
+            <div>
+              <label>接入方式</label>
+              <div className={'ellipsis'}>{props.transport || '--'}</div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </TableCard>
+  );
+};

+ 1 - 1
src/components/ProTableCard/CardItems/product.tsx

@@ -11,7 +11,7 @@ export interface ProductCardProps extends ProductItem {
   actions?: React.ReactNode[];
   avatarSize?: number;
 }
-const defaultImage = require('/public/images/device-type-3-big.png');
+const defaultImage = require('/public/images/device-product.png');
 
 export default (props: ProductCardProps) => {
   const intl = useIntl();

+ 8 - 5
src/components/ProTableCard/index.less

@@ -24,7 +24,6 @@
   .pro-table-card-items {
     display: grid;
     grid-gap: 26px;
-    grid-template-columns: repeat(4, 1fr);
     padding-bottom: 38px;
 
     .pro-table-card-item {
@@ -54,11 +53,15 @@
 
         .card-item-content {
           display: flex;
+          flex-wrap: wrap;
 
-          > div:last-child {
-            flex-grow: 1;
-            width: 0;
-            margin-left: 12px;
+          > div {
+            width: 50%;
+
+            &:nth-child(even) {
+              width: calc(50% - 12px);
+              margin-left: 12px;
+            }
           }
 
           label {

+ 5 - 1
src/components/ProTableCard/index.tsx

@@ -19,6 +19,7 @@ type ModelType = keyof typeof ModelEnum;
 
 interface ProTableCardProps<T> {
   cardRender?: (data: T) => JSX.Element | React.ReactNode;
+  gridColumn?: number;
 }
 
 const ProTableCard = <
@@ -43,7 +44,10 @@ const ProTableCard = <
     return (
       <>
         {dataSource && dataSource.length ? (
-          <div className={'pro-table-card-items'}>
+          <div
+            className={'pro-table-card-items'}
+            style={{ gridTemplateColumns: `repeat(${props.gridColumn || 4}, 1fr)` }}
+          >
             {dataSource.map((item) =>
               cardRender && isFunction(cardRender) ? cardRender(item) : null,
             )}

+ 23 - 0
src/components/RadioCard/index.less

@@ -71,4 +71,27 @@
       }
     }
   }
+
+  &.disabled {
+    .radio-card-item {
+      color: @disabled-color;
+      border-color: @disabled-bg;
+      cursor: not-allowed;
+
+      .checked-icon {
+        background-color: @disabled-active-bg;
+      }
+
+      &:hover,
+      &:focus {
+        color: @disabled-color;
+        border-color: @disabled-active-bg;
+      }
+
+      &.checked {
+        color: @disabled-color;
+        border-color: @disabled-active-bg;
+      }
+    }
+  }
 }

+ 12 - 6
src/components/RadioCard/index.tsx

@@ -1,4 +1,4 @@
-import { useEffect, useRef, useState } from 'react';
+import React, { useEffect, useRef, useState } from 'react';
 import classNames from 'classnames';
 import { isArray } from 'lodash';
 import './index.less';
@@ -9,14 +9,17 @@ type RadioCardModelType = 'multiple' | 'singular';
 interface RadioCardItem {
   label: string;
   value: string;
-  imgUrl: string;
+  imgUrl?: string;
 }
 
 export interface RadioCardProps {
+  options: RadioCardItem[];
   value?: string | string[];
   model?: RadioCardModelType;
-  options: RadioCardItem[];
+  itemStyle?: React.CSSProperties;
+  className?: string;
   onChange?: (keys: string | string[]) => void;
+  disabled?: boolean;
   onSelect?: (key: string, selected: boolean, node: RadioCardItem[]) => void;
 }
 
@@ -62,19 +65,22 @@ export default (props: RadioCardProps) => {
   };
 
   return (
-    <div className={'radio-card-items'}>
+    <div className={classNames('radio-card-items', props.className, { disabled: props.disabled })}>
       {options.map((item) => {
         return (
           <div
             className={classNames('radio-card-item', {
               checked: keys?.includes(item.value),
             })}
+            style={props.itemStyle}
             key={item.value}
             onClick={() => {
-              toggleOption(item.value);
+              if (!props.disabled) {
+                toggleOption(item.value);
+              }
             }}
           >
-            <img width={32} height={32} src={item.imgUrl} alt={''} />
+            {item.imgUrl && <img width={32} height={32} src={item.imgUrl} alt={''} />}
             <span>{item.label}</span>
             <div className={'checked-icon'}>
               <div>

+ 34 - 0
src/pages/media/Device/Save/ProviderSelect.tsx

@@ -0,0 +1,34 @@
+import classNames from 'classnames';
+
+interface ProviderProps {
+  value?: string;
+  options?: any[];
+  onChange?: (id: string) => void;
+  onSelect?: (id: string, rowData: any) => void;
+}
+
+export default (props: ProviderProps) => {
+  return (
+    <div className={'provider-list'}>
+      {props.options && props.options.length
+        ? props.options.map((item) => (
+            <div
+              onClick={() => {
+                if (props.onChange) {
+                  props.onChange(item.id);
+                }
+
+                if (props.onSelect) {
+                  props.onSelect(item.id, item);
+                }
+              }}
+              style={{ padding: 16 }}
+              className={classNames({ active: item.id === props.value })}
+            >
+              {item.name}
+            </div>
+          ))
+        : null}
+    </div>
+  );
+};

+ 135 - 0
src/pages/media/Device/Save/SaveProduct.tsx

@@ -0,0 +1,135 @@
+import { useEffect, useState } from 'react';
+import { service } from '../index';
+import { useRequest } from 'umi';
+import { Form, Input, message, Modal } from 'antd';
+import ProviderItem from './ProviderSelect';
+
+interface SaveProps {
+  visible: boolean;
+  reload: (id: string, data: any) => void;
+  type: string;
+  close?: () => void;
+}
+
+export default (props: SaveProps) => {
+  const { visible, close, reload } = props;
+  const [loading, setLoading] = useState(false);
+  const [form] = Form.useForm();
+
+  const { data: providerList, run: getProviderList } = useRequest(service.queryProvider, {
+    manual: true,
+    formatResult: (response) => response.result.data,
+  });
+
+  useEffect(() => {
+    if (visible) {
+      getProviderList({
+        terms: [{ column: 'provider', value: props.type }],
+      });
+    }
+  }, [visible]);
+
+  useEffect(() => {
+    if (form) {
+      form.setFieldsValue({ accessProvider: props.type });
+    }
+  }, [props.type]);
+
+  const onClose = () => {
+    form.resetFields();
+    if (close) {
+      close();
+    }
+  };
+
+  const onSubmit = async () => {
+    const formData = await form.validateFields();
+    if (formData) {
+      setLoading(true);
+      const resp = await service.saveProduct(formData);
+      if (resp.status === 200) {
+        //  新增成功之后 发布产品
+        const deployResp = await service.deployProductById(resp.result.id);
+        setLoading(false);
+        if (deployResp.status === 200) {
+          if (reload) {
+            reload(resp.result.id, resp.result.name);
+          }
+          onClose();
+        } else {
+          message.error('新增失败');
+        }
+      } else {
+        setLoading(false);
+        message.error('新增失败');
+      }
+    }
+  };
+
+  return (
+    <Modal
+      maskClosable={false}
+      mask={false}
+      visible={visible}
+      width={660}
+      confirmLoading={loading}
+      title={'快速添加'}
+      onOk={onSubmit}
+      onCancel={onClose}
+    >
+      <Form
+        form={form}
+        layout={'vertical'}
+        labelCol={{
+          style: { width: 100 },
+        }}
+      >
+        <Form.Item
+          name={'name'}
+          label={'产品名称'}
+          required
+          rules={[
+            { required: true, message: '请输入产品名称' },
+            { max: 64, message: '最多可输入64个字符' },
+          ]}
+        >
+          <Input placeholder={'请输入产品名称'} />
+        </Form.Item>
+        <Form.Item
+          name={'accessId'}
+          label={'接入网关'}
+          required
+          rules={[{ required: true, message: '请选择接入网关' }]}
+        >
+          <ProviderItem
+            options={providerList}
+            onSelect={(_, rowData) => {
+              form.setFieldsValue({
+                accessName: rowData.name,
+                protocolName: rowData.protocolDetail.name,
+                messageProtocol: rowData.protocolDetail.id,
+                transportProtocol: rowData.transportDetail.id,
+                accessProvider: rowData.provider,
+              });
+            }}
+          />
+        </Form.Item>
+        <Form.Item name={'accessName'} hidden>
+          <Input />
+        </Form.Item>
+        <Form.Item name={'messageProtocol'} hidden>
+          <Input />
+        </Form.Item>
+        <Form.Item name={'protocolName'} hidden>
+          <Input />
+        </Form.Item>
+        <Form.Item name={'transportProtocol'} hidden>
+          <Input />
+        </Form.Item>
+        <Form.Item name={'accessProvider'} hidden>
+          <Input />
+        </Form.Item>
+      </Form>
+    </Modal>
+  );
+};

+ 323 - 0
src/pages/media/Device/Save/index.tsx

@@ -0,0 +1,323 @@
+import { useEffect, useState } from 'react';
+import { Button, Col, Form, Input, message, Modal, Radio, Row, Select } from 'antd';
+import { useIntl } from 'umi';
+import { RadioCard, UploadImage } from '@/components';
+import { PlusOutlined } from '@ant-design/icons';
+import { service } from '../index';
+import SaveProductModal from './SaveProduct';
+import type { DeviceItem } from '../typings';
+
+interface SaveProps {
+  visible: boolean;
+  close: () => void;
+  reload: () => void;
+  data?: DeviceItem;
+  model: 'add' | 'edit';
+}
+
+const DefaultAccessType = 'gb28181-2016';
+
+export default (props: SaveProps) => {
+  const { visible, close, data } = props;
+  const intl = useIntl();
+  const [form] = Form.useForm();
+  const [loading, setLoading] = useState(false);
+  const [productVisible, setProductVisible] = useState(false);
+  const [accessType, setAccessType] = useState(DefaultAccessType);
+  const [productList, setProductList] = useState<any[]>([]);
+
+  const getProductList = async (productParams: any) => {
+    const resp = await service.queryProductList(productParams);
+    if (resp.status === 200) {
+      setProductList(resp.result);
+    }
+  };
+
+  const queryProduct = async (value: string) => {
+    getProductList({
+      terms: [
+        { column: 'accessProvider', value: value },
+        { column: 'state', value: 1 },
+      ],
+    });
+  };
+
+  useEffect(() => {
+    if (visible) {
+      if (props.model === 'edit') {
+        form.setFieldsValue(data);
+        const _accessType = data?.provider || DefaultAccessType;
+        setAccessType(_accessType);
+
+        queryProduct(_accessType);
+      } else {
+        form.setFieldsValue({
+          provider: DefaultAccessType,
+        });
+        queryProduct(DefaultAccessType);
+        setAccessType(DefaultAccessType);
+      }
+    }
+  }, [visible]);
+
+  const handleSave = async () => {
+    const formData = await form.validateFields();
+    if (formData) {
+      const { type, ...extraFormData } = formData;
+      setLoading(true);
+      const resp =
+        type === DefaultAccessType
+          ? await service.saveGB(extraFormData)
+          : await service.saveFixed(extraFormData);
+      setLoading(false);
+      if (resp.status === 200) {
+        if (props.reload) {
+          props.reload();
+        }
+        form.resetFields();
+        close();
+        message.success('操作成功');
+      } else {
+        message.error('操作失败');
+      }
+    }
+  };
+
+  const intlFormat = (
+    id: string,
+    defaultMessage: string,
+    paramsID?: string,
+    paramsMessage?: string,
+  ) => {
+    const paramsObj: Record<string, string> = {};
+    if (paramsID) {
+      const paramsMsg = intl.formatMessage({
+        id: paramsID,
+        defaultMessage: paramsMessage,
+      });
+      paramsObj.name = paramsMsg;
+    }
+
+    return intl.formatMessage(
+      {
+        id,
+        defaultMessage,
+      },
+      paramsObj,
+    );
+  };
+
+  console.log(productList);
+
+  return (
+    <>
+      <Modal
+        maskClosable={false}
+        visible={visible}
+        onCancel={() => {
+          form.resetFields();
+          close();
+        }}
+        width={610}
+        title={intl.formatMessage({
+          id: `pages.data.option.${props.model || 'add'}`,
+          defaultMessage: '新增',
+        })}
+        confirmLoading={loading}
+        onOk={handleSave}
+      >
+        <Form
+          form={form}
+          layout={'vertical'}
+          labelCol={{
+            style: { width: 100 },
+          }}
+        >
+          <Row>
+            <Col span={24}>
+              <Form.Item
+                name={'provider'}
+                label={'接入方式'}
+                required
+                rules={[{ required: true, message: '请选择接入方式' }]}
+              >
+                <RadioCard
+                  model={'singular'}
+                  itemStyle={{ width: '50%' }}
+                  onSelect={(key) => {
+                    setAccessType(key);
+                    queryProduct(key);
+                  }}
+                  disabled={props.model === 'edit'}
+                  options={[
+                    {
+                      label: 'GB/T28181',
+                      value: DefaultAccessType,
+                    },
+                    {
+                      label: '固定地址',
+                      value: 'fixed-media',
+                    },
+                  ]}
+                />
+              </Form.Item>
+            </Col>
+          </Row>
+          <Row>
+            <Col span={8}>
+              <Form.Item name={'photoUrl'}>
+                <UploadImage />
+              </Form.Item>
+            </Col>
+            <Col span={16}>
+              <Form.Item
+                label={'ID'}
+                name={'id'}
+                required
+                rules={[{ required: true, message: '请输入ID' }, {}]}
+              >
+                <Input placeholder={'请输入ID'} disabled={props.model === 'edit'} />
+              </Form.Item>
+              <Form.Item
+                label={'设备名称'}
+                name={'name'}
+                required
+                rules={[
+                  { required: true, message: '请输入名称' },
+                  { max: 64, message: '最多可输入64个字符' },
+                ]}
+              >
+                <Input placeholder={'请输入设备名称'} />
+              </Form.Item>
+            </Col>
+          </Row>
+          <Row>
+            <Col span={24}>
+              <Form.Item
+                label={'所属产品'}
+                required
+                rules={[{ required: true, message: '请选择所属产品' }]}
+              >
+                <Form.Item name={'productId'} noStyle>
+                  <Select
+                    fieldNames={{
+                      label: 'name',
+                      value: 'id',
+                    }}
+                    disabled={props.model === 'edit'}
+                    options={productList}
+                    placeholder={'请选择所属产品'}
+                    style={{ width: 'calc(100% - 36px)' }}
+                  />
+                </Form.Item>
+                <Form.Item noStyle>
+                  <Button
+                    type={'link'}
+                    style={{ padding: '4px 10px' }}
+                    onClick={() => {
+                      setProductVisible(true);
+                    }}
+                  >
+                    <PlusOutlined />
+                  </Button>
+                </Form.Item>
+              </Form.Item>
+            </Col>
+            {accessType === DefaultAccessType && (
+              <Col span={24}>
+                <Form.Item
+                  label={'接入密码'}
+                  name={'password'}
+                  required
+                  rules={[
+                    { required: true, message: '请输入接入密码' },
+                    { max: 64, message: '最大可输入64位' },
+                  ]}
+                >
+                  <Input.Password placeholder={'请输入接入密码'} />
+                </Form.Item>
+              </Col>
+            )}
+            {props.model === 'edit' && (
+              <>
+                <Col span={24}>
+                  <Form.Item
+                    label={'流传输模式'}
+                    name={'streamMode'}
+                    required
+                    rules={[{ required: true, message: '请选择流传输模式' }]}
+                  >
+                    <Radio.Group
+                      optionType="button"
+                      buttonStyle="solid"
+                      options={[
+                        { label: 'UDP', value: 'UDP' },
+                        { label: 'TCP', value: 'TCP' },
+                      ]}
+                    />
+                  </Form.Item>
+                </Col>
+                <Col span={24}>
+                  <Form.Item
+                    label={'设备厂商'}
+                    name={'manufacturer'}
+                    rules={[{ max: 64, message: '最多可输入64个字符' }]}
+                  >
+                    <Input placeholder={'请输入设备厂商'} />
+                  </Form.Item>
+                </Col>
+                <Col span={24}>
+                  <Form.Item
+                    label={'设备型号'}
+                    name={'model'}
+                    rules={[{ max: 64, message: '最多可输入64个字符' }]}
+                  >
+                    <Input placeholder={'请输入设备型号'} />
+                  </Form.Item>
+                </Col>
+                <Col span={24}>
+                  <Form.Item
+                    label={'固件版本'}
+                    name={'firmware'}
+                    rules={[{ max: 64, message: '最多可输入64个字符' }]}
+                  >
+                    <Input placeholder={'请输入固件版本'} />
+                  </Form.Item>
+                </Col>
+              </>
+            )}
+            <Col span={24}>
+              <Form.Item label={'说明'} name={'description'}>
+                <Input.TextArea
+                  placeholder={intlFormat('pages.form.tip.input', '请输入')}
+                  rows={4}
+                  style={{ width: '100%' }}
+                  maxLength={200}
+                  showCount={true}
+                />
+              </Form.Item>
+            </Col>
+          </Row>
+          <Form.Item name={'id'} hidden>
+            <Input />
+          </Form.Item>
+        </Form>
+      </Modal>
+      <SaveProductModal
+        visible={productVisible}
+        type={accessType}
+        close={() => {
+          setProductVisible(false);
+        }}
+        reload={(productId: string, name: string) => {
+          form.setFieldsValue({ productId });
+          productList.push({
+            id: productId,
+            name,
+          });
+          setProductList([...productList]);
+        }}
+      />
+    </>
+  );
+};

+ 4 - 0
src/pages/media/Device/Save/providerSelect.less

@@ -0,0 +1,4 @@
+.provider-list {
+  max-height: 450px;
+  overflow-y: auto;
+}

+ 156 - 19
src/pages/media/Device/index.tsx

@@ -1,20 +1,37 @@
 // 视频设备列表
 import { PageContainer } from '@ant-design/pro-layout';
-import { useRef } from 'react';
+import { useRef, useState } from 'react';
 import type { ActionType, ProColumns } from '@jetlinks/pro-table';
-import { Tooltip } from 'antd';
-import { ArrowDownOutlined, BugOutlined, EditOutlined, MinusOutlined } from '@ant-design/icons';
-import BaseCrud from '@/components/BaseCrud';
-import BaseService from '@/utils/BaseService';
+import { Button, message, Popconfirm, Tooltip } from 'antd';
+import {
+  ArrowDownOutlined,
+  BugOutlined,
+  DeleteOutlined,
+  EditOutlined,
+  EyeOutlined,
+  MinusOutlined,
+  PlusOutlined,
+  SyncOutlined,
+} from '@ant-design/icons';
 import type { DeviceItem } from '@/pages/media/Device/typings';
-import { useIntl } from '@@/plugin-locale/localeExports';
-import { BadgeStatus } from '@/components';
+import { useIntl, useHistory } from 'umi';
+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 Service from './service';
+import Save from './Save';
+
+export const service = new Service('media/device');
 
-export const service = new BaseService<DeviceItem>('media/device');
 const Device = () => {
   const intl = useIntl();
   const actionRef = useRef<ActionType>();
+  const [visible, setVisible] = useState<boolean>(false);
+  const [current, setCurrent] = useState<DeviceItem>();
+  const [queryParam, setQueryParam] = useState({});
+  const history = useHistory<Record<string, string>>();
 
   const columns: ProColumns<DeviceItem>[] = [
     {
@@ -95,7 +112,7 @@ const Device = () => {
         id: 'pages.searchTable.titleStatus',
         defaultMessage: '状态',
       }),
-      render: (text, record) => (
+      render: (_, record) => (
         <BadgeStatus
           status={record.state.value}
           statusNames={{
@@ -103,7 +120,7 @@ const Device = () => {
             offline: StatusColorEnum.error,
             notActive: StatusColorEnum.processing,
           }}
-          text={text}
+          text={record.state.text}
         />
       ),
     },
@@ -160,19 +177,139 @@ const Device = () => {
     },
   ];
 
-  const schema = {};
+  /**
+   * table 查询参数
+   * @param data
+   */
+  const searchFn = (data: any) => {
+    setQueryParam(data);
+  };
+
+  const deleteItem = async (id: string) => {
+    const response: any = await service.remove(id);
+    if (response.status === 200) {
+      message.success(
+        intl.formatMessage({
+          id: 'pages.data.option.success',
+          defaultMessage: '操作成功!',
+        }),
+      );
+    }
+    actionRef.current?.reload();
+  };
+
   return (
     <PageContainer>
-      <BaseCrud
+      <SearchComponent field={columns} onSearch={searchFn} />
+      <ProTableCard<DeviceItem>
         columns={columns}
-        service={service}
-        search={false}
-        title={intl.formatMessage({
-          id: 'pages.media.device',
-          defaultMessage: '模拟测试',
-        })}
-        schema={schema}
         actionRef={actionRef}
+        options={{ fullScreen: true }}
+        params={queryParam}
+        request={(params = {}) =>
+          service.query({
+            ...params,
+            sorts: [
+              {
+                name: 'createTime',
+                order: 'desc',
+              },
+            ],
+          })
+        }
+        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>,
+        ]}
+        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"
+                onClick={() => {
+                  setCurrent(record);
+                  setVisible(true);
+                }}
+                type={'link'}
+                style={{ padding: 0 }}
+              >
+                <EditOutlined />
+                {intl.formatMessage({
+                  id: 'pages.data.option.edit',
+                  defaultMessage: '编辑',
+                })}
+              </Button>,
+              <Button key={'viewChannel'}>查看通道</Button>,
+              <Button key={'updateChannel'} disabled={record.state.value === 'offline'}>
+                <SyncOutlined />
+                更新通道
+              </Button>,
+              <Popconfirm
+                key="delete"
+                title={intl.formatMessage({
+                  id:
+                    record.state.value === 'offline'
+                      ? 'pages.device.productDetail.deleteTip'
+                      : 'page.table.isDelete',
+                  defaultMessage: '是否删除?',
+                })}
+                onConfirm={async () => {
+                  if (record.state.value !== 'offline') {
+                    await deleteItem(record.id);
+                  } else {
+                    message.error('在线设备不能进行删除操作');
+                  }
+                }}
+              >
+                <Button
+                  type={'link'}
+                  style={{ padding: 0 }}
+                  disabled={record.state.value !== 'offline'}
+                >
+                  <DeleteOutlined />
+                </Button>
+              </Popconfirm>,
+            ]}
+          />
+        )}
+      />
+      <Save
+        model={!current ? 'add' : 'edit'}
+        data={current}
+        close={() => {
+          setVisible(false);
+        }}
+        reload={() => {
+          actionRef.current?.reload();
+        }}
+        visible={visible}
       />
     </PageContainer>
   );

+ 30 - 0
src/pages/media/Device/service.ts

@@ -0,0 +1,30 @@
+import BaseService from '@/utils/BaseService';
+import { request } from 'umi';
+import SystemConst from '@/utils/const';
+import type { DeviceItem } from './typings';
+
+class Service extends BaseService<DeviceItem> {
+  // 新增GB28181接入的设备
+  saveGB = (data?: any) => request(`${this.uri}/gb28181`, { method: 'PATCH', data });
+
+  // 新增固定地址接入的设备
+  saveFixed = (data?: any) => request(`${this.uri}/fixed-url`, { method: 'PATCH', data });
+
+  // 快速添加产品
+  saveProduct = (data?: any) =>
+    request(`/${SystemConst.API_BASE}/device/product`, { method: 'POST', data });
+
+  // 产品发布
+  deployProductById = (id: string) =>
+    request(`/${SystemConst.API_BASE}/device/product/${id}/deploy`, { method: 'POST' });
+
+  // 查询产品列表
+  queryProductList = (data?: any) =>
+    request(`/${SystemConst.API_BASE}/device/product/_query/no-paging`, { method: 'POST', data });
+
+  // 查询设备接入配置
+  queryProvider = (data?: any) =>
+    request(`/${SystemConst.API_BASE}/gateway/device/detail/_query`, { method: 'POST', data });
+}
+
+export default Service;

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

@@ -1,6 +1,7 @@
 import type { BaseItem, State } from '@/utils/typings';
 
-type DeviceItem = {
+export type DeviceItem = {
+  photoUrl?: string;
   channelNumber: number;
   createTime: number;
   firmware: string;