xieyonghong 3 lat temu
rodzic
commit
42aea4b026
65 zmienionych plików z 925 dodań i 592 usunięć
  1. BIN
      public/images/access/background.png
  2. BIN
      public/images/access/child-device.png
  3. BIN
      public/images/access/coap.png
  4. BIN
      public/images/access/ctwing.png
  5. BIN
      public/images/access/gb28181.png
  6. BIN
      public/images/access/http.png
  7. BIN
      public/images/access/modbus.png
  8. BIN
      public/images/access/mqtt-broke.png
  9. BIN
      public/images/access/mqtt.png
  10. BIN
      public/images/access/onenet.png
  11. BIN
      public/images/access/opc-ua.png
  12. BIN
      public/images/access/tcp.png
  13. BIN
      public/images/access/udp.png
  14. BIN
      public/images/access/video-device.png
  15. BIN
      public/images/access/websocket.png
  16. BIN
      public/images/media/cloud.png
  17. BIN
      public/images/media/doc1.png
  18. BIN
      public/images/media/doc2.png
  19. BIN
      public/images/media/doc3.png
  20. BIN
      public/images/media/doc4.png
  21. BIN
      public/images/northbound/aliyun3.png
  22. BIN
      public/images/northbound/aliyun4.png
  23. 4 4
      src/components/ProTableCard/CardItems/networkCard.tsx
  24. 7 0
      src/components/SearchComponent/index.tsx
  25. 3 2
      src/pages/Log/Access/index.tsx
  26. 1 1
      src/pages/Log/System/index.tsx
  27. 29 27
      src/pages/Northbound/AliCloud/Detail/index.less
  28. 11 2
      src/pages/Northbound/AliCloud/Detail/index.tsx
  29. 1 1
      src/pages/Northbound/DuerOS/Detail/index.tsx
  30. 45 2
      src/pages/account/Center/bind/index.tsx
  31. 24 8
      src/pages/device/Instance/Detail/index.tsx
  32. 4 3
      src/pages/device/Instance/Import/index.tsx
  33. 1 0
      src/pages/device/Instance/typings.d.ts
  34. 68 45
      src/pages/device/Product/Detail/Access/AccessConfig/index.tsx
  35. 18 10
      src/pages/device/Product/Detail/index.tsx
  36. 4 3
      src/pages/device/components/Metadata/Base/index.tsx
  37. 7 13
      src/pages/home/components/DeviceChoose.tsx
  38. 16 18
      src/pages/home/components/ProductChoose.tsx
  39. 14 4
      src/pages/home/comprehensive/index.tsx
  40. 11 3
      src/pages/home/device/index.tsx
  41. 11 2
      src/pages/home/init/index.less
  42. 2 2
      src/pages/home/init/index.tsx
  43. 3 1
      src/pages/home/ops/index.tsx
  44. 15 8
      src/pages/link/AccessConfig/Detail/Access/index.tsx
  45. 4 0
      src/pages/link/AccessConfig/Detail/Channel/index.tsx
  46. 61 27
      src/pages/link/AccessConfig/Detail/Provider/index.less
  47. 26 20
      src/pages/link/AccessConfig/Detail/Provider/index.tsx
  48. 4 1
      src/pages/link/Type/Detail/index.tsx
  49. 4 4
      src/pages/link/Type/index.tsx
  50. 45 3
      src/pages/media/Device/Playback/index.tsx
  51. 344 295
      src/pages/media/Device/Save/index.tsx
  52. 9 21
      src/pages/media/Device/index.tsx
  53. 2 0
      src/pages/media/Device/service.ts
  54. 1 1
      src/pages/media/Stream/index.tsx
  55. 3 1
      src/pages/notice/Config/BindUser/index.tsx
  56. 1 1
      src/pages/notice/Config/Detail/index.tsx
  57. 1 1
      src/pages/notice/Config/Log/index.tsx
  58. 37 14
      src/pages/notice/Config/SyncUser/index.tsx
  59. 65 30
      src/pages/notice/Config/index.tsx
  60. 9 8
      src/pages/notice/Template/Detail/index.tsx
  61. 6 4
      src/pages/notice/Template/Log/index.tsx
  62. 1 1
      src/pages/system/DataSource/Management/EditTable.tsx
  63. 1 1
      src/pages/user/Login/index.tsx
  64. 1 0
      src/utils/menu/index.ts
  65. 1 0
      src/utils/menu/router.ts

BIN
public/images/access/background.png


BIN
public/images/access/child-device.png


BIN
public/images/access/coap.png


BIN
public/images/access/ctwing.png


BIN
public/images/access/gb28181.png


BIN
public/images/access/http.png


BIN
public/images/access/modbus.png


BIN
public/images/access/mqtt-broke.png


BIN
public/images/access/mqtt.png


BIN
public/images/access/onenet.png


BIN
public/images/access/opc-ua.png


BIN
public/images/access/tcp.png


BIN
public/images/access/udp.png


BIN
public/images/access/video-device.png


BIN
public/images/access/websocket.png


BIN
public/images/media/cloud.png


BIN
public/images/media/doc1.png


BIN
public/images/media/doc2.png


BIN
public/images/media/doc3.png


BIN
public/images/media/doc4.png


BIN
public/images/northbound/aliyun3.png


BIN
public/images/northbound/aliyun4.png


+ 4 - 4
src/components/ProTableCard/CardItems/networkCard.tsx

@@ -19,8 +19,8 @@ export default (props: NoticeCardProps) => {
   const createDetail = () => {
     const record = props;
     if (record.shareCluster) {
-      const host = record.configuration.publicHost || record.configuration.remoteHost;
-      const port = record.configuration.publicPort || record.configuration.remotePort;
+      const host = record.configuration?.publicHost || record.configuration?.remoteHost;
+      const port = record.configuration?.publicPort || record.configuration?.remotePort;
       return host ? (
         <>
           {networkMap[record.type]}
@@ -30,8 +30,8 @@ export default (props: NoticeCardProps) => {
     } else {
       const log = record.cluster?.map(
         (item) =>
-          `${item.configuration.publicHost || record.configuration.remoteHost}:${
-            item.configuration.publicPort || record.configuration.remotePort
+          `${item.configuration?.publicHost || item.configuration?.remoteHost}:${
+            item.configuration?.publicPort || item.configuration?.remotePort
           }`,
       );
       return (

+ 7 - 0
src/components/SearchComponent/index.tsx

@@ -436,6 +436,13 @@ const SearchComponent = <T extends Record<string, any>>(props: Props<T>) => {
     uiParamRef.current = ui2Server(log);
     const _expand =
       (log.terms1 && log.terms1?.length > 1) || (log.terms2 && log.terms2?.length > 1);
+    console.log(
+      _expand,
+      '展开',
+      log,
+      (log.terms1 && log.terms1?.length > 1) || (log.terms2 && log.terms2?.length > 1),
+    );
+    console.log(log.terms1, log.terms2, 'log-terms');
     if (_expand) {
       setExpand(false);
     }

+ 3 - 2
src/pages/Log/Access/index.tsx

@@ -92,8 +92,9 @@ const Access = () => {
         id: 'pages.log.access.requestUser',
         defaultMessage: '请求用户',
       }),
-      dataIndex: 'username',
-      render: (text, record: any) => <Tag color="geekblue">{record?.context?.username || ''}</Tag>,
+      dataIndex: 'context.username',
+      render: (text, record: any) =>
+        record?.context?.username ? <Tag color="geekblue">{record?.context?.username}</Tag> : '',
     },
     {
       title: intl.formatMessage({

+ 1 - 1
src/pages/Log/System/index.tsx

@@ -68,7 +68,7 @@ const System = () => {
         id: 'pages.log.system.serviceName',
         defaultMessage: '服务名',
       }),
-      dataIndex: 'server',
+      dataIndex: 'context.server',
       width: 150,
       ellipsis: true,
       render: (text, record) => record?.context?.server || '--',

+ 29 - 27
src/pages/Northbound/AliCloud/Detail/index.less

@@ -1,35 +1,37 @@
-.doc {
-  height: 750px;
-  padding: 24px;
-  overflow-y: auto;
-  color: rgba(#000, 0.8);
-  font-size: 14px;
-  background-color: #fafafa;
+.aliyun {
+  .doc {
+    height: 1000px;
+    padding: 24px;
+    overflow-y: auto;
+    color: rgba(#000, 0.8);
+    font-size: 14px;
+    background-color: #fafafa;
 
-  .url {
-    padding: 8px 16px;
-    color: #2f54eb;
-    background-color: rgba(#a7bdf7, 0.2);
-  }
+    .url {
+      padding: 8px 16px;
+      color: #2f54eb;
+      background-color: rgba(#a7bdf7, 0.2);
+    }
 
-  h1 {
-    margin: 16px 0;
-    color: rgba(#000, 0.85);
-    font-weight: bold;
-    font-size: 14px;
+    h1 {
+      margin: 16px 0;
+      color: rgba(#000, 0.85);
+      font-weight: bold;
+      font-size: 14px;
 
-    &:first-child {
-      margin-top: 0;
+      &:first-child {
+        margin-top: 0;
+      }
     }
-  }
 
-  h2 {
-    margin: 6px 0;
-    color: rgba(0, 0, 0, 0.8);
-    font-size: 14px;
-  }
+    h2 {
+      margin: 6px 0;
+      color: rgba(0, 0, 0, 0.8);
+      font-size: 14px;
+    }
 
-  .image {
-    margin: 16px 0;
+    .image {
+      margin: 16px 0;
+    }
   }
 }

+ 11 - 2
src/pages/Northbound/AliCloud/Detail/index.tsx

@@ -441,7 +441,7 @@ const Detail = observer(() => {
               </FormButtonGroup.Sticky>
             </Form>
           </Col>
-          <Col span={10}>
+          <Col span={10} className="aliyun">
             <div className="doc">
               <div className="url">
                 阿里云物联网平台:
@@ -467,6 +467,10 @@ const Detail = observer(() => {
                 <div>
                   阿里云内部给每台机器设置的唯一编号。请根据购买的阿里云服务器地址进行选择。
                 </div>
+                <div>获取路径:“阿里云物联网平台”--“服务地址”</div>
+                <div className={'image'}>
+                  <Image width="100%" src={require('/public/images/northbound/aliyun3.png')} />
+                </div>
                 <h2> 2、AccesskeyID/Secret</h2>
                 <div>
                   用于程序通知方式调用云服务费API的用户标识和秘钥获取路径:“阿里云管理控制台”--“用户头像”--“”--“AccessKey管理”--“查看”
@@ -479,7 +483,12 @@ const Detail = observer(() => {
                   物联网平台对于阿里云物联网平台,是一个网关设备,需要映射到阿里云物联网平台的具体产品
                 </div>
                 <h2> 4. 产品映射</h2>
-                <div>将阿里云物联网平台中的产品实例与物联网平台的产品实例进行关联</div>
+                <div>
+                  将阿里云物联网平台中的产品实例与物联网平台的产品实例进行关联。关联后需要进入该产品下的每一个设备的实例信息页,填入对应的阿里云物联网平台设备的DeviceName、DeviceSecret进行一对一绑定。
+                </div>
+                <div className={'image'}>
+                  <Image width="100%" src={require('/public/images/northbound/aliyun4.png')} />
+                </div>
               </div>
             </div>
           </Col>

+ 1 - 1
src/pages/Northbound/DuerOS/Detail/index.tsx

@@ -647,7 +647,7 @@ const Save = () => {
     history.back();
   };
   return (
-    <PageContainer className={'page-title-show'}>
+    <PageContainer>
       <Card>
         <Row>
           <Col span={12}>

+ 45 - 2
src/pages/account/Center/bind/index.tsx

@@ -1,4 +1,4 @@
-import { Button, Card, Form, Input } from 'antd';
+import { Button, Card, Form, Input, message } from 'antd';
 import { useEffect, useState } from 'react';
 import Service from '@/pages/account/Center/service';
 import api from '@/pages/user/Login/service';
@@ -6,6 +6,7 @@ import styles from './index.less';
 import Token from '@/utils/token';
 import { useModel } from '@@/plugin-model/useModel';
 import { onlyMessage } from '@/utils/util';
+import { catchError, filter, mergeMap } from 'rxjs/operators';
 
 export const service = new Service();
 
@@ -16,6 +17,7 @@ const Bind = () => {
   const [code, setCode] = useState<string>('');
   const [isLogin, setIslogin] = useState<any>('yes');
   const { initialState, setInitialState } = useModel('@@initialState');
+  const [captcha, setCaptcha] = useState<{ key?: string; base64?: string }>({});
 
   const bindPage = require('/public/images/bind/bindPage.png');
   const Vector = require('/public/images/bind/Vector.png');
@@ -38,6 +40,16 @@ const Bind = () => {
       setUser(res?.result);
     });
   };
+  const getCode = () => {
+    api
+      .captchaConfig()
+      .pipe(
+        filter((r) => r.enabled),
+        mergeMap(api.getCaptcha),
+        catchError(() => message.error('服务端挂了!')),
+      )
+      .subscribe(setCaptcha);
+  };
 
   //未登录页
   const loginDiv = () => (
@@ -66,6 +78,29 @@ const Bind = () => {
           >
             <Input.Password />
           </Form.Item>
+          {captcha.key && (
+            <Form.Item
+              label="验证码"
+              name="verifyCode"
+              rules={[{ required: true, message: '请输入验证码' }]}
+            >
+              <Input
+                placeholder="请输入验证码"
+                addonAfter={
+                  <>
+                    <img
+                      style={{ width: 110 }}
+                      src={captcha.base64}
+                      alt="验证码"
+                      onClick={() => {
+                        getCode();
+                      }}
+                    />
+                  </>
+                }
+              />
+            </Form.Item>
+          )}
         </Form>
       </div>
     </div>
@@ -80,6 +115,7 @@ const Bind = () => {
       });
     }
   };
+
   const doLogin = async (data: any) => {
     api.login(data).subscribe({
       next: async (userInfo) => {
@@ -90,8 +126,12 @@ const Bind = () => {
         setTimeout(() => window.close(), 1000);
       },
       error: () => {
+        getCode();
         onlyMessage('登录失败,请重试', 'error');
       },
+      complete: () => {
+        getCode();
+      },
     });
   };
 
@@ -107,6 +147,7 @@ const Bind = () => {
       setIslogin(localStorage.getItem('onLogin'));
     }
   }, []);
+  useEffect(getCode, []);
 
   return (
     <>
@@ -180,12 +221,14 @@ const Bind = () => {
                   if (data) {
                     doLogin({
                       ...data,
+                      verifyKey: captcha.key,
                       bindCode: code,
+                      expires: 3600000,
                     });
                   }
                 }}
               >
-                登录并已绑定账户
+                登陆并绑定账户
               </Button>
             ) : (
               <Button

+ 24 - 8
src/pages/device/Instance/Detail/index.tsx

@@ -147,19 +147,35 @@ const InstanceDetail = observer(() => {
       tab: '设备诊断',
       component: <Diagnose />,
     },
-    {
-      key: 'metadata-map',
-      tab: '物模型映射',
-      component: <MetadataMap type="device" />,
-    },
+  ];
+
+  const pList = [
+    'websocket-server',
+    'http-server-gateway',
+    'udp-device-gateway',
+    'coap-server-gateway',
+    'mqtt-client-gateway',
+    'mqtt-server-gateway',
+    'tcp-server-gateway',
   ];
   const [list, setList] =
     useState<{ key: string; tab: string | ReactNode; component: ReactNode }[]>(baseList);
 
-  const getDetail = (id: string) => {
-    service.detail(id).then((response) => {
+  const getDetail = async (id: string) => {
+    const response = await service.detail(id);
+    if (response.status === 200) {
       InstanceModel.detail = response?.result;
       const datalist = [...baseList];
+      if (
+        InstanceModel.detail?.accessProvider &&
+        pList.includes(InstanceModel.detail?.accessProvider)
+      ) {
+        datalist.push({
+          key: 'metadata-map',
+          tab: '物模型映射',
+          component: <MetadataMap type="device" />,
+        });
+      }
       if (response.result.protocol === 'modbus-tcp') {
         datalist.push({
           key: 'modbus',
@@ -186,7 +202,7 @@ const InstanceDetail = observer(() => {
       // 写入物模型数据
       const metadata: DeviceMetadata = JSON.parse(response.result?.metadata || '{}');
       MetadataAction.insert(metadata);
-    });
+    }
   };
 
   const [subscribeTopic] = useSendWebsocketMessage();

+ 4 - 3
src/pages/device/Instance/Import/index.tsx

@@ -106,13 +106,12 @@ const NormalUpload = (props: any) => {
       <Space>
         <Upload
           action={`/${SystemConst.API_BASE}/file/static`}
-          accept={`.${props?.fileType?.fileType || 'xlsx'}`}
+          accept={'.xlsx, .csv'}
           headers={{
             'X-Access-Token': Token.get(),
           }}
           onChange={async (info) => {
             if (info.file.status === 'done') {
-              onlyMessage('上传成功');
               const resp: any = info.file.response || { result: '' };
               await submitData(resp?.result || '');
             }
@@ -201,9 +200,11 @@ const Import = (props: Props) => {
         });
       });
       onFieldValueChange('fileType', (field) => {
+        const product = form.getValuesIn('product') || '';
         form.setFieldState('*(upload)', (state) => {
           state.componentProps = {
             fileType: field.value,
+            product,
           };
         });
       });
@@ -277,7 +278,7 @@ const Import = (props: Props) => {
         </Button>,
       ]}
     >
-      <div style={{ marginTop: '20px' }}>
+      <div style={{ marginTop: '10px' }}>
         <FormProvider form={form}>
           <SchemaField schema={schema} />
         </FormProvider>

+ 1 - 0
src/pages/device/Instance/typings.d.ts

@@ -40,6 +40,7 @@ export type DeviceInstance = {
   tags: any;
   photoUrl: string;
   independentMetadata?: boolean;
+  accessProvider?: string;
 };
 
 type Unit = {

+ 68 - 45
src/pages/device/Product/Detail/Access/AccessConfig/index.tsx

@@ -11,6 +11,7 @@ import AccessConfigCard from '@/components/ProTableCard/CardItems/AccessConfig';
 import { getMenuPathByCode } from '@/utils/menu';
 import PermissionButton from '@/components/PermissionButton';
 import { onlyMessage } from '@/utils/util';
+import Empty from '@/components/Empty';
 
 interface Props {
   close: () => void;
@@ -28,7 +29,7 @@ const AccessConfig = (props: Props) => {
     pageIndex: 0,
     total: 0,
   });
-  const [param, setParam] = useState<any>({ pageSize: 4 });
+  const [param, setParam] = useState<any>({ pageSize: 4, terms: [] });
 
   const [currrent, setCurrrent] = useState<any>({
     id: productModel.current?.accessId,
@@ -42,11 +43,27 @@ const AccessConfig = (props: Props) => {
 
   const handleSearch = (params: any) => {
     setParam(params);
-    service
-      .queryList({ ...params, sorts: [{ name: 'createTime', order: 'desc' }] })
-      .then((resp) => {
-        setDataSource(resp?.result);
-      });
+    const temp = {
+      ...params,
+      terms:
+        productModel.current?.deviceType?.value === 'childrenDevice'
+          ? [
+              ...params.terms,
+              {
+                terms: [
+                  {
+                    column: 'provider',
+                    termType: 'eq',
+                    value: 'child-device',
+                  },
+                ],
+              },
+            ]
+          : [...params?.terms],
+    };
+    service.queryList({ ...temp, sorts: [{ name: 'createTime', order: 'desc' }] }).then((resp) => {
+      setDataSource(resp?.result);
+    });
   };
 
   const columns: ProColumns<any>[] = [
@@ -157,46 +174,52 @@ const AccessConfig = (props: Props) => {
           </PermissionButton>
         </div>
       </div>
-      <Row gutter={[16, 16]}>
-        {(dataSource?.data || []).map((item: any) => (
-          <Col
-            key={item.name}
-            span={12}
-            onClick={() => {
-              setCurrrent(item);
+      {dataSource?.data?.length > 0 ? (
+        <Row gutter={[16, 16]}>
+          {(dataSource?.data || []).map((item: any) => (
+            <Col
+              key={item.name}
+              span={12}
+              onClick={() => {
+                setCurrrent(item);
+              }}
+            >
+              <AccessConfigCard
+                {...item}
+                showTool={false}
+                activeStyle={currrent?.id === item.id ? 'active' : ''}
+              />
+            </Col>
+          ))}
+        </Row>
+      ) : (
+        <Empty />
+      )}
+      {dataSource?.data?.length > 0 && (
+        <div style={{ display: 'flex', marginTop: 20, justifyContent: 'flex-end' }}>
+          <Pagination
+            showSizeChanger
+            size="small"
+            className={'pro-table-card-pagination'}
+            total={dataSource?.total || 0}
+            current={dataSource?.pageIndex + 1}
+            onChange={(page, size) => {
+              handleSearch({
+                ...param,
+                pageIndex: page - 1,
+                pageSize: size,
+              });
             }}
-          >
-            <AccessConfigCard
-              {...item}
-              showTool={false}
-              activeStyle={currrent?.id === item.id ? 'active' : ''}
-            />
-          </Col>
-        ))}
-      </Row>
-      <div style={{ display: 'flex', marginTop: 20, justifyContent: 'flex-end' }}>
-        <Pagination
-          showSizeChanger
-          size="small"
-          className={'pro-table-card-pagination'}
-          total={dataSource?.total || 0}
-          current={dataSource?.pageIndex + 1}
-          onChange={(page, size) => {
-            handleSearch({
-              ...param,
-              pageIndex: page - 1,
-              pageSize: size,
-            });
-          }}
-          pageSizeOptions={[4, 8, 16, 32]}
-          pageSize={dataSource?.pageSize}
-          showTotal={(num) => {
-            const minSize = dataSource?.pageIndex * dataSource?.pageSize + 1;
-            const MaxSize = (dataSource?.pageIndex + 1) * dataSource?.pageSize;
-            return `第 ${minSize} - ${MaxSize > num ? num : MaxSize} 条/总共 ${num} 条`;
-          }}
-        />
-      </div>
+            pageSizeOptions={[4, 8, 16, 32]}
+            pageSize={dataSource?.pageSize}
+            showTotal={(num) => {
+              const minSize = dataSource?.pageIndex * dataSource?.pageSize + 1;
+              const MaxSize = (dataSource?.pageIndex + 1) * dataSource?.pageSize;
+              return `第 ${minSize} - ${MaxSize > num ? num : MaxSize} 条/总共 ${num} 条`;
+            }}
+          />
+        </div>
+      )}
     </Modal>
   );
 };

+ 18 - 10
src/pages/device/Product/Detail/index.tsx

@@ -144,16 +144,6 @@ const ProductDetail = observer(() => {
         const metadata: DeviceMetadata = JSON.parse(data.metadata);
         MetadataAction.insert(metadata);
       }
-      if (data?.accessProvider && pList.includes(data?.accessProvider)) {
-        setList([
-          ...initList,
-          {
-            key: 'metadata-map',
-            tab: '物模型映射',
-            component: <MetadataMap type="product" />,
-          },
-        ]);
-      }
       service.instanceCount(encodeQuery({ terms: { productId: param?.id } })).then((res: any) => {
         if (res.status === 200) {
           productModel.current = { ...data, count: res.result };
@@ -163,6 +153,24 @@ const ProductDetail = observer(() => {
   };
 
   useEffect(() => {
+    if (
+      productModel.current?.accessProvider &&
+      pList.includes(productModel.current?.accessProvider)
+    ) {
+      setList([
+        ...initList,
+        {
+          key: 'metadata-map',
+          tab: '物模型映射',
+          component: <MetadataMap type="product" />,
+        },
+      ]);
+    } else {
+      setList([...initList]);
+    }
+  }, [productModel.current]);
+
+  useEffect(() => {
     const queryParam = new URLSearchParams(location.search);
     const _mode = queryParam.get('type');
     if (_mode) {

+ 4 - 3
src/pages/device/components/Metadata/Base/index.tsx

@@ -156,9 +156,9 @@ const BaseMetadata = observer((props: Props) => {
         columns={MetadataMapping.get(type)!.concat(actions)}
         rowKey="id"
         search={false}
-        pagination={{
-          pageSize: 5,
-        }}
+        // pagination={{
+        //   pageSize: 5,
+        // }}
         options={{
           density: false,
           fullScreen: false,
@@ -168,6 +168,7 @@ const BaseMetadata = observer((props: Props) => {
         }}
         toolbar={{
           search: {
+            placeholder: '请输入标识',
             onSearch: handleSearch,
           },
         }}

+ 7 - 13
src/pages/home/components/DeviceChoose.tsx

@@ -6,7 +6,7 @@ import SearchComponent from '@/components/SearchComponent';
 import type { DeviceItem } from '@/pages/media/Home/typings';
 import { useEffect, useRef, useState } from 'react';
 import { useIntl } from '@@/plugin-locale/localeExports';
-import { BadgeStatus, PermissionButton } from '@/components';
+import { BadgeStatus } from '@/components';
 import { StatusColorEnum } from '@/components/BadgeStatus';
 import useHistory from '@/hooks/route/useHistory';
 import { getMenuPathByParams, MENUS_CODE } from '@/utils/menu';
@@ -20,7 +20,6 @@ interface DeviceModalProps {
 export default (props: DeviceModalProps) => {
   const intl = useIntl();
   const history = useHistory();
-  const permission = PermissionButton.usePermission('device/Instance').permission;
 
   const actionRef = useRef<ActionType>();
   const [searchParam, setSearchParam] = useState({});
@@ -103,17 +102,12 @@ export default (props: DeviceModalProps) => {
       onCancel={cancel}
       onOk={() => {
         if (deviceItem?.id) {
-          if (!!permission.update) {
-            history.push(
-              `${getMenuPathByParams(MENUS_CODE['device/Instance/Detail'], deviceItem.id)}`,
-              {
-                tab: 'diagnose',
-              },
-            );
-          } else {
-            message.warning('暂无权限,请联系管理员');
-            cancel();
-          }
+          history.push(
+            `${getMenuPathByParams(MENUS_CODE['device/Instance/Detail'], deviceItem.id)}`,
+            {
+              tab: 'diagnose',
+            },
+          );
         } else {
           message.warning('请选择设备');
         }

+ 16 - 18
src/pages/home/components/ProductChoose.tsx

@@ -1,12 +1,10 @@
 import { FormItem, FormLayout, Select } from '@formily/antd';
 import { createForm } from '@formily/core';
 import { createSchemaField, FormProvider } from '@formily/react';
-import { Button, message, Modal } from 'antd';
+import { Button, Modal } from 'antd';
 import 'antd/lib/tree-select/style/index.less';
 import { useEffect, useState } from 'react';
 import { service } from '@/pages/device/Instance';
-import encodeQuery from '@/utils/encodeQuery';
-import { PermissionButton } from '@/components';
 import useHistory from '@/hooks/route/useHistory';
 import { getMenuPathByParams, MENUS_CODE } from '@/utils/menu';
 
@@ -16,7 +14,6 @@ interface Props {
 }
 
 const ProductChoose = (props: Props) => {
-  const productPermission = PermissionButton.usePermission('device/Product').permission;
   const { visible, close } = props;
   const [productList, setProductList] = useState<any[]>([]);
 
@@ -29,16 +26,20 @@ const ProductChoose = (props: Props) => {
   });
 
   useEffect(() => {
-    service.getProductList(encodeQuery({ paging: false, terms: { state: 1 } })).then((resp) => {
-      if (resp.status === 200) {
-        const list = resp.result.map((item: { name: any; id: any }) => ({
-          label: item.name,
-          value: item.id,
-        }));
-        setProductList(list);
-      }
-    });
-  }, []);
+    if (visible) {
+      service.getProductList().then((resp) => {
+        if (resp.status === 200) {
+          const list = resp.result
+            .filter((i: any) => !i?.accessId)
+            .map((item: { name: any; id: any }) => ({
+              label: item.name,
+              value: item.id,
+            }));
+          setProductList(list);
+        }
+      });
+    }
+  }, [visible]);
 
   const form = createForm({});
 
@@ -91,16 +92,13 @@ const ProductChoose = (props: Props) => {
           onClick={async () => {
             const data: any = await form.submit();
             const path = getMenuPathByParams(`device/Product/Detail`);
-            if (path && !!productPermission.update) {
+            if (path) {
               history.push(
                 `${getMenuPathByParams(MENUS_CODE['device/Product/Detail'], data.product)}`,
                 {
                   tab: 'access',
                 },
               );
-            } else {
-              message.warning('暂无权限,请联系管理员');
-              close();
             }
           }}
         >

+ 14 - 4
src/pages/home/comprehensive/index.tsx

@@ -256,9 +256,13 @@ const Comprehensive = () => {
             {
               title: '配置产品接入方式',
               content:
-                '通过产品对同一类型的所有设备进行统一的接入方式配置。请参照设备铭牌说明选择匹配的接入方式。',
+                '通过产品对同一类型的设备进行统一的接入方式配置。请参照设备铭牌说明选择匹配的接入方式。',
               onClick: () => {
-                setProductVisible(true);
+                if (!!productPermission.update) {
+                  setProductVisible(true);
+                } else {
+                  message.warning('暂无权限,请联系管理员');
+                }
               },
             },
             {
@@ -279,7 +283,11 @@ const Comprehensive = () => {
               title: '功能调试',
               content: '对添加的测试设备进行功能调试,验证能否连接到平台,设备功能是否配置正确。',
               onClick: () => {
-                setDeviceVisible(true);
+                if (!!devicePermission.update) {
+                  setDeviceVisible(true);
+                } else {
+                  message.warning('暂无权限,请联系管理员');
+                }
               },
             },
             {
@@ -369,7 +377,9 @@ const Comprehensive = () => {
               onClick: () => {
                 const url = getMenuPathByCode(MENUS_CODE['Log']);
                 if (!!url) {
-                  history.push(url);
+                  history.push(url, {
+                    key: 'system',
+                  });
                 } else {
                   message.warning('暂无权限,请联系管理员');
                 }

+ 11 - 3
src/pages/home/device/index.tsx

@@ -153,9 +153,13 @@ const Device = () => {
             {
               title: '配置产品接入方式',
               content:
-                '通过产品对同一类型的所有设备进行统一的接入方式配置。请参照设备铭牌说明选择匹配的接入方式。',
+                '通过产品对同一类型的设备进行统一的接入方式配置。请参照设备铭牌说明选择匹配的接入方式。',
               onClick: () => {
-                setProductVisible(true);
+                if (!!productPermission.update) {
+                  setProductVisible(true);
+                } else {
+                  message.warning('暂无权限,请联系管理员');
+                }
               },
             },
             {
@@ -176,7 +180,11 @@ const Device = () => {
               title: '功能调试',
               content: '对添加的测试设备进行功能调试,验证能否连接到平台,设备功能是否配置正确。',
               onClick: () => {
-                setDeviceVisible(true);
+                if (!!devicePermission.update) {
+                  setDeviceVisible(true);
+                } else {
+                  message.warning('暂无权限,请联系管理员');
+                }
               },
             },
             {

+ 11 - 2
src/pages/home/init/index.less

@@ -1,5 +1,14 @@
+.homeBox {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  width: 100%;
+  height: calc(100vh - 150px);
+  background-color: white;
+}
 .title {
-  margin-bottom: 10px;
+  margin-top: 48px;
+  margin-bottom: 48px;
   font-weight: 400;
   font-size: 26px;
   text-align: center;
@@ -12,6 +21,6 @@
 
   img {
     width: 80%;
-    margin-bottom: 5px;
+    margin-bottom: 10px;
   }
 }

+ 2 - 2
src/pages/home/init/index.tsx

@@ -28,7 +28,7 @@ const Init = (props: Props) => {
     },
   ];
   return (
-    <div>
+    <div className={styles.homeBox}>
       <div className={styles.title}>请选择首页视图</div>
       <Radio.Group value={value} onChange={(e) => setValue(e.target.value)}>
         <Row gutter={24}>
@@ -43,7 +43,7 @@ const Init = (props: Props) => {
         </Row>
       </Radio.Group>
 
-      <div style={{ textAlign: 'center', marginTop: 30 }}>
+      <div style={{ textAlign: 'center', marginTop: 48 }}>
         <Button
           type="primary"
           onClick={() => {

+ 3 - 1
src/pages/home/ops/index.tsx

@@ -194,7 +194,9 @@ const Ops = () => {
               onClick: () => {
                 const url = getMenuPathByCode(MENUS_CODE['Log']);
                 if (!!url) {
-                  history.push(url);
+                  history.push(url, {
+                    key: 'system',
+                  });
                 } else {
                   message.warning('暂无权限,请联系管理员');
                 }

+ 15 - 8
src/pages/link/AccessConfig/Detail/Access/index.tsx

@@ -253,6 +253,7 @@ const Access = (props: Props) => {
               <Input.Search
                 key={'network'}
                 placeholder="请输入名称"
+                allowClear
                 onSearch={(value: string) => {
                   queryNetworkList(
                     props.provider?.id,
@@ -310,14 +311,18 @@ const Access = (props: Props) => {
                         <Tooltip
                           placement="topLeft"
                           title={
-                            <div>
-                              {[...item.addresses].map((i: any) => (
-                                <div key={i.address}>
-                                  <Badge color={i.health === -1 ? 'red' : 'green'} />
-                                  {i.address}
-                                </div>
-                              ))}
-                            </div>
+                            item.addresses?.length > 1 ? (
+                              <div>
+                                {[...item.addresses].map((i: any) => (
+                                  <div key={i.address}>
+                                    <Badge color={i.health === -1 ? 'red' : 'green'} />
+                                    {i.address}
+                                  </div>
+                                ))}
+                              </div>
+                            ) : (
+                              ''
+                            )
                           }
                         >
                           <div
@@ -333,6 +338,7 @@ const Access = (props: Props) => {
                             {item.addresses.slice(0, 1).map((i: any) => (
                               <div className={styles.item} key={i.address}>
                                 <Badge color={i.health === -1 ? 'red' : 'green'} text={i.address} />
+                                {item.addresses?.length > 1 && '...'}
                               </div>
                             ))}
                           </div>
@@ -392,6 +398,7 @@ const Access = (props: Props) => {
             <div className={styles.search}>
               <Input.Search
                 key={'protocol'}
+                allowClear
                 placeholder="请输入名称"
                 onSearch={(value: string) => {
                   queryProcotolList(

+ 4 - 0
src/pages/link/AccessConfig/Detail/Channel/index.tsx

@@ -90,6 +90,10 @@ const Media = (props: Props) => {
                       if (resp.status === 200) {
                         onlyMessage('操作成功!');
                         history.goBack();
+                        if ((window as any).onTabSaveSuccess) {
+                          (window as any).onTabSaveSuccess(resp);
+                          setTimeout(() => window.close(), 300);
+                        }
                       }
                     } catch (errorInfo) {
                       console.error('Failed:', errorInfo);

+ 61 - 27
src/pages/link/AccessConfig/Detail/Provider/index.less

@@ -1,29 +1,63 @@
-.images {
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  width: 64px;
-  height: 64px;
-  color: white;
-  font-size: 12px;
-  text-align: center;
-  background: linear-gradient(
-    128.453709216706deg,
-    rgba(255, 255, 255, 1) 4%,
-    rgba(113, 187, 255, 1) 43%,
-    rgba(24, 144, 255, 1) 100%
-  );
-  border: 1px solid rgba(242, 242, 242, 1);
-  border-radius: 50%;
-}
-
-.desc {
+.provider {
+  position: relative;
   width: 100%;
-  margin-top: 10px;
-  overflow: hidden;
-  color: rgba(0, 0, 0, 0.55);
-  font-weight: 400;
-  font-size: 13px;
-  white-space: nowrap;
-  text-overflow: ellipsis;
+  padding: 20px;
+  background: url('/images/access/background.png') no-repeat;
+  background-size: 100% 100%;
+  border: 1px solid #e6e6e6;
+
+  &::before {
+    position: absolute;
+    top: 0;
+    left: 40px;
+    display: block;
+    width: 15%;
+    min-width: 64px;
+    height: 2px;
+    background-image: url('/images/rectangle.png');
+    background-repeat: no-repeat;
+    background-size: 100% 100%;
+    content: ' ';
+  }
+
+  &:hover {
+    box-shadow: 0 0 24px rgba(#000, 0.1);
+  }
+
+  .box {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    width: 100%;
+
+    .left {
+      display: flex;
+      width: calc(100% - 70px);
+
+      .images {
+        width: 64px;
+        height: 64px;
+
+        img {
+          width: 100%;
+        }
+      }
+
+      .context {
+        width: calc(100% - 84px);
+        margin: 10px;
+
+        .desc {
+          width: 100%;
+          margin-top: 10px;
+          overflow: hidden;
+          color: rgba(0, 0, 0, 0.55);
+          font-weight: 400;
+          font-size: 13px;
+          white-space: nowrap;
+          text-overflow: ellipsis;
+        }
+      }
+    }
+  }
 }

+ 26 - 20
src/pages/link/AccessConfig/Detail/Provider/index.tsx

@@ -52,33 +52,39 @@ const Provider = (props: Props) => {
     ]);
   }, [props.data]);
 
+  const backMap = new Map();
+  backMap.set('mqtt-server-gateway', require('/public/images/access/mqtt.png'));
+  backMap.set('websocket-server', require('/public/images/access/websocket.png'));
+  backMap.set('modbus-tcp', require('/public/images/access/modbus.png'));
+  backMap.set('coap-server-gateway', require('/public/images/access/coap.png'));
+  backMap.set('tcp-server-gateway', require('/public/images/access/tcp.png'));
+  backMap.set('Ctwing', require('/public/images/access/ctwing.png'));
+  backMap.set('child-device', require('/public/images/access/child-device.png'));
+  backMap.set('opc-ua', require('/public/images/access/opc-ua.png'));
+  backMap.set('http-server-gateway', require('/public/images/access/http.png'));
+  backMap.set('fixed-media', require('/public/images/access/video-device.png'));
+  backMap.set('udp-device-gateway', require('/public/images/access/udp.png'));
+  backMap.set('OneNet', require('/public/images/access/onenet.png'));
+  backMap.set('gb28181-2016', require('/public/images/access/gb28181.png'));
+  backMap.set('mqtt-client-gateway', require('/public/images/access/mqtt-broke.png'));
+
   return (
     <div>
       {dataSource.map((i) => (
         <Card key={i.type} style={{ marginTop: 20 }}>
           <TitleComponent data={i.title} />
-          <Row gutter={[16, 16]}>
+          <Row gutter={[24, 24]}>
             {(i?.list || []).map((item: any) => (
               <Col key={item.name} span={12}>
-                <Card style={{ width: '100%' }} hoverable>
-                  <div
-                    style={{
-                      width: '100%',
-                      display: 'flex',
-                      alignItems: 'center',
-                      justifyContent: 'space-between',
-                    }}
-                  >
-                    <div
-                      style={{
-                        display: 'flex',
-                        width: 'calc(100% - 70px)',
-                      }}
-                    >
-                      <div className={styles.images}>{item.name}</div>
-                      <div style={{ margin: '10px', width: 'calc(100% - 84px)' }}>
+                <div className={styles.provider}>
+                  <div className={styles.box}>
+                    <div className={styles.left}>
+                      <div className={styles.images}>
+                        <img src={backMap.get(item.id)} />
+                      </div>
+                      <div className={styles.context}>
                         <div style={{ fontWeight: 600 }}>{item.name}</div>
-                        <div className={styles.desc}>{item?.description || '--'}</div>
+                        <div className={styles.desc}>{item?.description || ''}</div>
                       </div>
                     </div>
                     <div style={{ width: '70px' }}>
@@ -92,7 +98,7 @@ const Provider = (props: Props) => {
                       </Button>
                     </div>
                   </div>
-                </Card>
+                </div>
               </Col>
             ))}
           </Row>

+ 4 - 1
src/pages/link/Type/Detail/index.tsx

@@ -142,6 +142,9 @@ const Save = observer(() => {
               });
             } else {
               // 独立配置
+              f5.setFieldState('grid.cluster.cluster', (state) => {
+                state.value = [{}];
+              });
             }
           });
           onFieldValueChange('grid.cluster.cluster.*.layout2.serverId', async (field, f3) => {
@@ -229,7 +232,7 @@ const Save = observer(() => {
           layout: 'vertical',
         },
         'x-component-props': {
-          placeholder: '请输入节点名称',
+          placeholder: '请选择节点名称',
         },
         'x-reactions': [
           {

+ 4 - 4
src/pages/link/Type/index.tsx

@@ -95,8 +95,8 @@ const Network = () => {
       hideInSearch: true,
       renderText: (text, record) => {
         if (record.shareCluster) {
-          const host = record.configuration.publicHost || record.configuration.remoteHost;
-          const port = record.configuration.publicPort || record.configuration.remotePort;
+          const host = record.configuration?.publicHost || record.configuration?.remoteHost;
+          const port = record.configuration?.publicPort || record.configuration?.remotePort;
           return host ? (
             <>
               {networkMap[record.type]}
@@ -106,8 +106,8 @@ const Network = () => {
         } else {
           const log = record.cluster?.map(
             (item) =>
-              `${item.configuration.publicHost || record.configuration.remoteHost}:${
-                item.configuration.publicPort || record.configuration.remotePort
+              `${item.configuration?.publicHost || item.configuration?.remoteHost}:${
+                item.configuration?.publicPort || item.configuration?.remotePort
               }`,
           );
           return (

+ 45 - 3
src/pages/media/Device/Playback/index.tsx

@@ -2,7 +2,7 @@
 import { PageContainer } from '@ant-design/pro-layout';
 import LivePlayer from '@/components/Player';
 import { useCallback, useEffect, useRef, useState } from 'react';
-import { Calendar, Empty, List, Select, Tooltip } from 'antd';
+import { Calendar, Empty, List, Tooltip } from 'antd';
 import { useLocation } from 'umi';
 import Service from './service';
 import './index.less';
@@ -17,8 +17,10 @@ import {
   LoadingOutlined,
   PauseCircleOutlined,
   PlayCircleOutlined,
+  QuestionCircleOutlined,
 } from '@ant-design/icons';
 import TimeLine from './timeLine';
+import { RadioCard } from '@/components';
 
 const service = new Service('media');
 
@@ -238,7 +240,47 @@ export default () => {
           />
         </div>
         <div className={'playback-right'}>
-          <Select
+          <Tooltip
+            // title={<>云端:存储在服务器中 本地:存储在设备本地</>}
+            title={
+              <>
+                <div>云端:存储在服务器中</div>
+                <div>本地:存储在设备本地</div>
+              </>
+            }
+            placement="topLeft"
+          >
+            <div>
+              类型: <QuestionCircleOutlined />
+            </div>
+          </Tooltip>
+          <RadioCard
+            model={'singular'}
+            value={type}
+            itemStyle={{ minWidth: 0, width: '100%' }}
+            onSelect={(key: string) => {
+              setType(key);
+              console.log(key);
+              if (key === 'cloud') {
+                queryServiceRecords(time!);
+              } else {
+                queryLocalRecords(time!);
+              }
+            }}
+            options={[
+              {
+                label: '云端',
+                value: 'cloud',
+                imgUrl: require('/public/images/media/cloud.png'),
+              },
+              {
+                label: '本地',
+                value: 'local',
+                imgUrl: require('/public/images/local.png'),
+              },
+            ]}
+          />
+          {/* <Select
             value={type}
             options={[
               { label: '云端', value: 'cloud' },
@@ -253,7 +295,7 @@ export default () => {
                 queryLocalRecords(time!);
               }
             }}
-          />
+          /> */}
           <div className={'playback-calendar'}>
             <Calendar
               value={time}

+ 344 - 295
src/pages/media/Device/Save/index.tsx

@@ -1,33 +1,32 @@
 import { useCallback, useEffect, useState } from 'react';
-import { Button, Col, Form, Input, Modal, Radio, Row, Select, Tooltip } from 'antd';
-import { useIntl } from 'umi';
+import { Button, Card, Col, Form, Input, Radio, Row, Select, Tooltip, Image } from 'antd';
+import { useIntl, useLocation } 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';
 import { getButtonPermission } from '@/utils/menu';
 import { onlyMessage } from '@/utils/util';
-
-interface SaveProps {
-  visible: boolean;
-  close: () => void;
-  reload: () => void;
-  data?: DeviceItem;
-  model: 'add' | 'edit';
-}
+import { PageContainer } from '@ant-design/pro-layout';
+import styles from '../../Cascade/Save/index.less';
+import { useDomFullHeight } from '@/hooks';
 
 const DefaultAccessType = 'gb28181-2016';
 
-export default (props: SaveProps) => {
-  const { visible, close, data } = props;
+const Save = () => {
+  const location: any = useLocation();
+  const id = location?.query?.id;
   const intl = useIntl();
   const [form] = Form.useForm();
-  const [loading, setLoading] = useState(false);
+  const { minHeight } = useDomFullHeight(`.mediaDevice`);
   const [productVisible, setProductVisible] = useState(false);
   const [accessType, setAccessType] = useState(DefaultAccessType);
   const [productList, setProductList] = useState<any[]>([]);
   const [oldPassword, setOldPassword] = useState('');
+  const img1 = require('/public/images/media/doc1.png');
+  const img2 = require('/public/images/media/doc2.png');
+  const img3 = require('/public/images/media/doc3.png');
+  const img4 = require('/public/images/media/doc4.png');
 
   const getProductList = async (productParams: any) => {
     const resp = await service.queryProductList(productParams);
@@ -46,23 +45,23 @@ export default (props: SaveProps) => {
   };
 
   useEffect(() => {
-    if (visible) {
-      setOldPassword('');
-      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);
-      }
+    if (id) {
+      service.getDetail(id).then((res) => {
+        if (res.status === 200) {
+          form.setFieldsValue(res.result);
+          const _accessType = res.result?.provider || DefaultAccessType;
+          setAccessType(_accessType);
+          queryProduct(_accessType);
+        }
+      });
+    } else {
+      form.setFieldsValue({
+        provider: DefaultAccessType,
+      });
+      queryProduct(DefaultAccessType);
+      setAccessType(DefaultAccessType);
     }
-  }, [visible]);
+  }, []);
 
   const handleSave = useCallback(async () => {
     const formData = await form.validateFields();
@@ -71,296 +70,346 @@ export default (props: SaveProps) => {
       if (formData.password === oldPassword) {
         delete extraFormData.password;
       }
-      setLoading(true);
       const resp =
         provider === DefaultAccessType
           ? await service.saveGB(extraFormData)
           : await service.saveFixed(extraFormData);
-      setLoading(false);
       if (resp.status === 200) {
-        if (props.reload) {
-          props.reload();
-        }
         form.resetFields();
-        close();
         onlyMessage('操作成功');
+        history.back();
       } else {
         onlyMessage('操作失败', 'error');
       }
     }
-  }, [props.model, oldPassword]);
+  }, [oldPassword]);
 
-  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;
-    }
+  // 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,
-    );
-  };
+  //   return intl.formatMessage(
+  //     {
+  //       id,
+  //       defaultMessage,
+  //     },
+  //     paramsObj,
+  //   );
+  // };
 
   return (
     <>
-      <Modal
-        maskClosable={false}
-        visible={visible}
-        onCancel={() => {
-          form.resetFields();
-          close();
-        }}
-        width={640}
-        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' },
-                  {
-                    pattern: /^[a-zA-Z0-9_\-]+$/,
-                    message: intl.formatMessage({
-                      id: 'pages.form.tip.id',
-                      defaultMessage: '请输入英文或者数字或者-或者_',
-                    }),
-                  },
-                  {
-                    max: 64,
-                    message: intl.formatMessage({
-                      id: 'pages.form.tip.max64',
-                      defaultMessage: '最多输入64个字符',
-                    }),
-                  },
-                ]}
+      <PageContainer>
+        <Card className="mediaDevice" style={{ minHeight }}>
+          <Row gutter={24}>
+            <Col span={12}>
+              <Form
+                form={form}
+                layout={'vertical'}
+                onFinish={() => {
+                  handleSave();
+                }}
+                labelCol={{
+                  style: { width: 100 },
+                }}
               >
-                <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: props.model === 'edit' ? '100%' : 'calc(100% - 36px)' }}
-                    onSelect={(_: any, node: any) => {
-                      const pwd = node.configuration ? node.configuration.access_pwd : '';
-                      form.setFieldsValue({
-                        password: pwd,
-                      });
-                      setOldPassword(pwd);
-                    }}
-                  />
-                </Form.Item>
-                {props.model !== 'edit' && (
-                  <Form.Item noStyle>
-                    {getButtonPermission('device/Product', 'add') ? (
-                      <Tooltip title={'暂无权限,请联系管理员'}>
-                        <Button type={'link'} style={{ padding: '4px 10px' }} disabled>
-                          <PlusOutlined />
-                        </Button>
-                      </Tooltip>
-                    ) : (
-                      <Button
-                        type={'link'}
-                        style={{ padding: '4px 10px' }}
-                        onClick={() => {
-                          setProductVisible(true);
+                <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={id}
+                        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' },
+                        {
+                          pattern: /^[a-zA-Z0-9_\-]+$/,
+                          message: intl.formatMessage({
+                            id: 'pages.form.tip.id',
+                            defaultMessage: '请输入英文或者数字或者-或者_',
+                          }),
+                        },
+                        {
+                          max: 64,
+                          message: intl.formatMessage({
+                            id: 'pages.form.tip.max64',
+                            defaultMessage: '最多输入64个字符',
+                          }),
+                        },
+                      ]}
+                    >
+                      <Input placeholder={'请输入ID'} disabled={!!id} />
+                    </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={!!id}
+                          options={productList}
+                          placeholder={'请选择所属产品'}
+                          style={{ width: id ? '100%' : 'calc(100% - 36px)' }}
+                          onSelect={(_: any, node: any) => {
+                            const pwd = node.configuration ? node.configuration.access_pwd : '';
+                            form.setFieldsValue({
+                              password: pwd,
+                            });
+                            setOldPassword(pwd);
+                          }}
+                        />
+                      </Form.Item>
+                      {!id && (
+                        <Form.Item noStyle>
+                          {getButtonPermission('device/Product', 'add') ? (
+                            <Tooltip title={'暂无权限,请联系管理员'}>
+                              <Button type={'link'} style={{ padding: '4px 10px' }} disabled>
+                                <PlusOutlined />
+                              </Button>
+                            </Tooltip>
+                          ) : (
+                            <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位' },
+                        ]}
                       >
-                        <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={'请输入接入密码'} />
+                        <Input.Password placeholder={'请输入接入密码'} />
+                      </Form.Item>
+                    </Col>
+                  )}
+                  {id && (
+                    <>
+                      <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_PASSIVE' },
+                            ]}
+                          />
+                        </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.props',
+                        //   '请输入',
+                        //   'pages.table.describe',
+                        //   '说明',
+                        // )}
+                        placeholder="请输入说明"
+                        rows={4}
+                        style={{ width: '100%' }}
+                        maxLength={200}
+                        showCount={true}
+                      />
+                    </Form.Item>
+                  </Col>
+                </Row>
+                <Form.Item name={'id'} hidden>
+                  <Input />
                 </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_PASSIVE' },
-                      ]}
-                    />
-                  </Form.Item>
-                </Col>
                 <Col span={24}>
-                  <Form.Item
-                    label={'设备厂商'}
-                    name={'manufacturer'}
-                    rules={[{ max: 64, message: '最多可输入64个字符' }]}
-                  >
-                    <Input placeholder={'请输入设备厂商'} />
+                  <Form.Item>
+                    <Button type="primary" htmlType="submit">
+                      保存
+                    </Button>
                   </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.props',
-                    '请输入',
-                    'pages.table.describe',
-                    '说明',
-                  )}
-                  rows={4}
-                  style={{ width: '100%' }}
-                  maxLength={200}
-                  showCount={true}
-                />
-              </Form.Item>
+              </Form>
+            </Col>
+            <Col span={12}>
+              <div className={styles.doc} style={{ height: 800 }}>
+                <h1>1、概述</h1>
+                <div>
+                  视频设备通过GB/T28181接入平台整体分为2部分,包括平台端配置和设备端配置,不同的设备端配置的路径或页面存在差异,但配置项基本大同小异。
+                </div>
+                <h1>2、配置说明</h1>
+                <h1>平台端配置</h1>
+                <h2>1、ID</h2>
+                <div>设备唯一标识,若不填写,系统将自动生成唯一标识</div>
+                <h2>2、所属产品</h2>
+                <div>
+                  只能选择接入方式为GB/T28281的产品,若当前无对应产品,可点击右侧快速添加按钮,填写产品名称和选择GB/T28181类型的网关完成产品创建
+                </div>
+                <h2>3、接入密码</h2>
+                <div>
+                  配置接入密码,设备端配置的密码需与该密码一致。该字段可在产品-设备接入页面进行统一配置,配置后所有设备将继承产品配置。设备单独修改后将脱离继承关系。
+                </div>
+                <h1>设备端配置</h1>
+                <div>
+                  各个厂家、不同设备型号的设备端配置页面布局存在差异,但配置项基本大同小异,此处以大华摄像头为例作为接入配置示例
+                </div>
+                <div className={styles.image}>
+                  <Image width="100%" src={img1} />
+                </div>
+                <h2>1、SIP服务器编号/SIP域</h2>
+                <div>
+                  SIP服务器编号填入该设备所属产品-接入方式页面“连接信息”的SIP。
+                  SIP域通常为SIP服务器编号的前10位。
+                </div>
+                <div className={styles.image}>
+                  <Image width="100%" src={img2} />
+                </div>
+                <h2>2、SIP服务器IP/端口</h2>
+                <div>SIP服务器IP/端口填入该设备所属产品-接入方式页面中“连接信息”的IP/端口。</div>
+                <div className={styles.image}>
+                  <Image width="100%" src={img3} />
+                </div>
+                <h2>3、设备编号</h2>
+                <div>
+                  设备编号为设备唯一性标识,物联网平台的设备接入没有校验该字段,输入任意数字均不影响设备接入平台。
+                </div>
+                <h2>4、注册密码</h2>
+                <div>填入该设备所属产品-接入方式页面中“GB28281配置”处的接入密码</div>
+                <div className={styles.image}>
+                  <Image width="100%" src={img4} />
+                </div>
+                <h2>5、其他字段</h2>
+                <div>不影响设备接入平台,可保持设备初始化值。</div>
+              </div>
             </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]);
-        }}
-      />
+        </Card>
+        <SaveProductModal
+          visible={productVisible}
+          type={accessType}
+          close={() => {
+            setProductVisible(false);
+          }}
+          reload={(productId: string, name: string) => {
+            form.setFieldsValue({ productId });
+            productList.push({
+              id: productId,
+              name,
+            });
+            setProductList([...productList]);
+          }}
+        />
+      </PageContainer>
     </>
   );
 };
+export default Save;

+ 9 - 21
src/pages/media/Device/index.tsx

@@ -1,6 +1,6 @@
 // 视频设备列表
 import { PageContainer } from '@ant-design/pro-layout';
-import { useEffect, useRef, useState } from 'react';
+import { useRef, useState } from 'react';
 import type { ActionType, ProColumns } from '@jetlinks/pro-table';
 import { Button, Tooltip } from 'antd';
 import {
@@ -14,13 +14,11 @@ import {
 import type { DeviceItem } from '@/pages/media/Device/typings';
 import { useHistory, useIntl } from 'umi';
 import { BadgeStatus, PermissionButton, ProTableCard } from '@/components';
-import useLocation from '@/hooks/route/useLocation';
 import { StatusColorEnum } from '@/components/BadgeStatus';
 import SearchComponent from '@/components/SearchComponent';
 import MediaDevice from '@/components/ProTableCard/CardItems/mediaDevice';
 import { getMenuPathByCode, getMenuPathByParams, MENUS_CODE } from '@/utils/menu';
 import Service from './service';
-import Save from './Save';
 import { onlyMessage } from '@/utils/util';
 
 export const service = new Service('media/device');
@@ -38,19 +36,9 @@ export const ProviderValue = {
 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 { permission } = PermissionButton.usePermission('media/Device');
-  const location = useLocation();
-
-  useEffect(() => {
-    const { state } = location;
-    if (state && state.save) {
-      setVisible(true);
-    }
-  }, [location]);
 
   /**
    * table 查询参数
@@ -210,8 +198,8 @@ const Device = () => {
           style={{ padding: 0 }}
           type={'link'}
           onClick={() => {
-            setCurrent(record);
-            setVisible(true);
+            const url = getMenuPathByCode(MENUS_CODE[`media/Device/Save`]);
+            history.push(url + `?id=${record.id}`);
           }}
         >
           <EditOutlined />
@@ -330,8 +318,8 @@ const Device = () => {
         headerTitle={[
           <PermissionButton
             onClick={() => {
-              setCurrent(undefined);
-              setVisible(true);
+              const url = getMenuPathByCode(MENUS_CODE[`media/Device/Save`]);
+              history.push(url);
             }}
             key="button"
             icon={<PlusOutlined />}
@@ -364,8 +352,8 @@ const Device = () => {
                 key="edit"
                 isPermission={permission.update}
                 onClick={() => {
-                  setCurrent(record);
-                  setVisible(true);
+                  const url = getMenuPathByCode(MENUS_CODE[`media/Device/Save`]);
+                  history.push(url + `?id=${record.id}`);
                 }}
                 type={'link'}
                 style={{ padding: 0 }}
@@ -445,7 +433,7 @@ const Device = () => {
           />
         )}
       />
-      <Save
+      {/* <Save
         model={!current ? 'add' : 'edit'}
         data={current}
         close={() => {
@@ -455,7 +443,7 @@ const Device = () => {
           actionRef.current?.reload();
         }}
         visible={visible}
-      />
+      /> */}
     </PageContainer>
   );
 };

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

@@ -28,6 +28,8 @@ class Service extends BaseService<DeviceItem> {
   // 查询设备接入配置
   queryProvider = (data?: any) =>
     request(`/${SystemConst.API_BASE}/gateway/device/detail/_query`, { method: 'POST', data });
+  //视频设备详情
+  getDetail = (id: string) => request(`${this.uri}/${id}`, { method: 'GET' });
 }
 
 export default Service;

+ 1 - 1
src/pages/media/Stream/index.tsx

@@ -26,7 +26,7 @@ const Stream = () => {
   const [param, setParam] = useState<any>({ pageSize: 10, terms: [] });
   const permissionCode = 'media/Stream';
   const { permission } = PermissionButton.usePermission(permissionCode);
-  const { minHeight } = useDomFullHeight(`.stream`, 36);
+  const { minHeight } = useDomFullHeight(`.stream`, 24);
 
   const columns: ProColumns<StreamItem>[] = [
     {

+ 3 - 1
src/pages/notice/Config/BindUser/index.tsx

@@ -60,6 +60,8 @@ const BindUser = (props: Props) => {
         'x-component': 'Select',
         'x-component-props': {
           placeholder: '请选择用户',
+          showSearch: true,
+          showArrow: true,
           filterOption: (input: string, option: any) =>
             option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0,
         },
@@ -76,7 +78,7 @@ const BindUser = (props: Props) => {
 
   return (
     <Modal
-      title="绑定用户 "
+      title="绑定用户"
       onCancel={() => {
         props.close();
       }}

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

@@ -539,7 +539,7 @@ const Detail = observer(() => {
                   add: {
                     type: 'void',
                     'x-component': 'ArrayTable.Addition',
-                    title: '添加条目',
+                    title: '添加',
                   },
                 },
               },

+ 1 - 1
src/pages/notice/Config/Log/index.tsx

@@ -81,7 +81,7 @@ const Log = observer(() => {
       footer={null}
       onCancel={() => (state.log = false)}
       title="通知记录"
-      width="50vw"
+      width="65vw"
       visible={state.log && !!state.current?.id}
     >
       <SearchComponent

+ 37 - 14
src/pages/notice/Config/SyncUser/index.tsx

@@ -1,4 +1,16 @@
-import { Badge, Button, Col, Input, Modal, Popconfirm, Row, Spin, Tooltip, Tree } from 'antd';
+import {
+  Badge,
+  Button,
+  Col,
+  Empty,
+  Input,
+  Modal,
+  Popconfirm,
+  Row,
+  Spin,
+  Tooltip,
+  Tree,
+} from 'antd';
 import { observer } from '@formily/react';
 import { service, state } from '..';
 import type { ActionType, ProColumns } from '@jetlinks/pro-table';
@@ -143,6 +155,11 @@ const SyncUser = observer(() => {
       title="同步用户"
       bodyStyle={{ height: '600px', overflowY: 'auto' }}
       visible={true}
+      footer={[
+        <Button key="back" onClick={() => (state.syncUser = false)}>
+          关闭
+        </Button>,
+      ]}
       onCancel={() => (state.syncUser = false)}
       width="80vw"
     >
@@ -155,21 +172,25 @@ const SyncUser = observer(() => {
                 style={{ marginBottom: 8 }}
                 placeholder="请输入部门名称"
               />
-              <Tree
-                fieldNames={{
-                  title: 'name',
-                  key: 'id',
-                }}
-                selectedKeys={[dept || '']}
-                onSelect={(key) => {
-                  setDept(key[0] as string);
-                }}
-                treeData={treeData}
-              />
+              {treeData && treeData.length !== 0 ? (
+                <Tree
+                  fieldNames={{
+                    title: 'name',
+                    key: 'id',
+                  }}
+                  selectedKeys={[dept || '']}
+                  onSelect={(key) => {
+                    setDept(key[0] as string);
+                  }}
+                  treeData={treeData}
+                />
+              ) : (
+                <Empty />
+              )}
             </div>
           </Col>
           <Col span={20}>
-            {dept && (
+            {dept ? (
               <ProTable
                 rowKey="thirdPartyUserId"
                 actionRef={actionRef}
@@ -227,10 +248,12 @@ const SyncUser = observer(() => {
                       }
                     }}
                   >
-                    <Button type="primary">保存</Button>
+                    <Button type="primary">自动绑定</Button>
                   </Popconfirm>
                 }
               />
+            ) : (
+              <Empty />
             )}
           </Col>
         </Row>

+ 65 - 30
src/pages/notice/Config/index.tsx

@@ -8,10 +8,11 @@ import {
   EditOutlined,
   EyeOutlined,
   PlusOutlined,
+  SmallDashOutlined,
   TeamOutlined,
   UnorderedListOutlined,
 } from '@ant-design/icons';
-import { Space, Upload } from 'antd';
+import { Dropdown, Menu, Space, Upload } from 'antd';
 import { useRef, useState } from 'react';
 import { useIntl } from '@@/plugin-locale/localeExports';
 import { downloadObject, onlyMessage } from '@/utils/util';
@@ -351,20 +352,20 @@ const Config = observer(() => {
               </div>
             }
             actions={[
-              (record.provider === 'dingTalkMessage' || record.provider === 'corpMessage') && (
-                <PermissionButton
-                  key="syncUser"
-                  isPermission={true}
-                  type="link"
-                  onClick={() => {
-                    state.syncUser = true;
-                    state.current = record;
-                  }}
-                >
-                  <TeamOutlined />
-                  同步用户
-                </PermissionButton>
-              ),
+              // (record.provider === 'dingTalkMessage' || record.provider === 'corpMessage') && (
+              //   <PermissionButton
+              //     key="syncUser"
+              //     isPermission={true}
+              //     type="link"
+              //     onClick={() => {
+              //       state.syncUser = true;
+              //       state.current = record;
+              //     }}
+              //   >
+              //     <TeamOutlined />
+              //     同步用户
+              //   </PermissionButton>
+              // ),
               <PermissionButton
                 isPermission={configPermission.update}
                 type={'link'}
@@ -391,20 +392,6 @@ const Config = observer(() => {
               </PermissionButton>,
               <PermissionButton
                 type={'link'}
-                key="export"
-                isPermission={configPermission.export}
-                onClick={() =>
-                  downloadObject(
-                    record,
-                    `通知配置${record.name}-${moment(new Date()).format('YYYY/MM/DD HH:mm:ss')}`,
-                  )
-                }
-              >
-                <ArrowDownOutlined />
-                导出
-              </PermissionButton>,
-              <PermissionButton
-                type={'link'}
                 key="log"
                 isPermission={configPermission.log}
                 onClick={() => {
@@ -415,7 +402,55 @@ const Config = observer(() => {
                 <UnorderedListOutlined />
                 通知记录
               </PermissionButton>,
-
+              <Dropdown
+                key={'more'}
+                placement="bottom"
+                overlay={
+                  <Menu>
+                    <Menu.Item key="export">
+                      <PermissionButton
+                        type={'link'}
+                        key="export"
+                        isPermission={configPermission.export}
+                        onClick={() =>
+                          downloadObject(
+                            record,
+                            `通知配置${record.name}-${moment(new Date()).format(
+                              'YYYY/MM/DD HH:mm:ss',
+                            )}`,
+                          )
+                        }
+                      >
+                        <ArrowDownOutlined />
+                        导出
+                      </PermissionButton>
+                      ,
+                    </Menu.Item>
+                    {(record.provider === 'dingTalkMessage' ||
+                      record.provider === 'corpMessage') && (
+                      <Menu.Item key="syncUser">
+                        <PermissionButton
+                          key="syncUser"
+                          isPermission={true}
+                          type="link"
+                          onClick={() => {
+                            state.syncUser = true;
+                            state.current = record;
+                          }}
+                        >
+                          <TeamOutlined />
+                          同步用户
+                        </PermissionButton>
+                      </Menu.Item>
+                    )}
+                  </Menu>
+                }
+              >
+                <PermissionButton type={'link'} isPermission={true} key="other">
+                  <SmallDashOutlined />
+                  其他
+                </PermissionButton>
+              </Dropdown>,
               <PermissionButton
                 key="delete"
                 isPermission={configPermission.delete}

+ 9 - 8
src/pages/notice/Template/Detail/index.tsx

@@ -783,8 +783,9 @@ const Detail = observer(() => {
                       minColumns: 2,
                     },
                     properties: {
-                      userIdList: {
-                        title: '收信人',
+                      departmentIdList: {
+                        title: '收信部门',
+                        required: true,
                         'x-component': 'Select',
                         'x-decorator': 'FormItem',
                         'x-decorator-props': {
@@ -792,17 +793,17 @@ const Detail = observer(() => {
                           gridSpan: 1,
                         },
                         'x-component-props': {
-                          placeholder: '请选择收信',
+                          placeholder: '请选择收信部门',
                         },
                         // 'x-reactions': {
                         //   dependencies: ['configId'],
                         //   fulfill: {
-                        //     run: '{{useAsyncDataSource(getDingTalkUser($deps[0]))}}',
+                        //     run: '{{useAsyncDataSource(getDingTalkDept($deps[0]))}}',
                         //   },
                         // },
                       },
-                      departmentIdList: {
-                        title: '收信部门',
+                      userIdList: {
+                        title: '收信',
                         'x-component': 'Select',
                         'x-decorator': 'FormItem',
                         'x-decorator-props': {
@@ -810,12 +811,12 @@ const Detail = observer(() => {
                           gridSpan: 1,
                         },
                         'x-component-props': {
-                          placeholder: '请选择收信部门',
+                          placeholder: '请选择收信',
                         },
                         // 'x-reactions': {
                         //   dependencies: ['configId'],
                         //   fulfill: {
-                        //     run: '{{useAsyncDataSource(getDingTalkDept($deps[0]))}}',
+                        //     run: '{{useAsyncDataSource(getDingTalkUser($deps[0]))}}',
                         //   },
                         // },
                       },

+ 6 - 4
src/pages/notice/Template/Log/index.tsx

@@ -84,7 +84,7 @@ const Log = observer(() => {
       destroyOnClose
       onCancel={() => (state.log = false)}
       title="通知记录"
-      width={'50vw'}
+      width={'65vw'}
       visible={state.log && !!state.current?.id}
     >
       <SearchComponent
@@ -99,9 +99,11 @@ const Log = observer(() => {
       />
       <ProTable<LogItem>
         search={false}
-        pagination={{
-          pageSize: 5,
-        }}
+        pagination={
+          {
+            // pageSize: 5,
+          }
+        }
         params={param}
         columns={columns}
         request={async (params) => service.getHistoryLog(state.current?.id || '', params)}

+ 1 - 1
src/pages/system/DataSource/Management/EditTable.tsx

@@ -135,7 +135,7 @@ const EditTable = (props: Props) => {
                     // },
                     {
                       maximum: 99999,
-                      minimum: 1,
+                      minimum: 0,
                     },
                   ],
                   // required: true,

+ 1 - 1
src/pages/user/Login/index.tsx

@@ -60,7 +60,7 @@ const Login: React.FC = () => {
       .pipe(
         filter((r) => r.enabled),
         mergeMap(Service.getCaptcha),
-        catchError(() => message.error('服务端挂了!')),
+        catchError(() => message.error('系统开小差,请稍后重试')),
       )
       .subscribe(setCaptcha);
   };

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

@@ -26,6 +26,7 @@ const extraRouteObj = {
   },
   'media/Device': {
     children: [
+      { code: 'Save', name: '详情' },
       { code: 'Channel', name: '通道列表' },
       { code: 'Playback', name: '回放' },
     ],

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

@@ -53,6 +53,7 @@ export enum MENUS_CODE {
   'media/Cascade/Channel' = 'media/Cascade/Channel',
   'media/Config' = 'media/Config',
   'media/Device' = 'media/Device',
+  'media/Device/Save' = 'media/Device/Save',
   'media/Device/Channel' = 'media/Device/Channel',
   'media/Device/Playback' = 'media/Device/Playback',
   'media/Reveal' = 'media/Reveal',