Bläddra i källkod

fix: 修改设备详情

sun-chaochao 3 år sedan
förälder
incheckning
3292761177

BIN
public/images/access-config-diaabled.png


BIN
public/images/access-config-enabled.png


+ 78 - 0
src/components/ProTableCard/CardItems/AccessConfig/index.less

@@ -0,0 +1,78 @@
+.tableCardDisabled {
+  width: 100%;
+  background: url('/images/access-config-diaabled.png') no-repeat;
+  background-size: 100% 100%;
+}
+
+.tableCardEnabled {
+  width: 100%;
+  background: url('/images/access-config-enabled.png') no-repeat;
+  background-size: 100% 100%;
+}
+
+.context-access {
+  display: flex;
+  width: 100%;
+  .card {
+    display: flex;
+    flex-direction: column;
+    width: 100%;
+    margin-left: 20px;
+    .header {
+      .title {
+        width: 90%;
+        overflow: hidden;
+        font-weight: 700;
+        font-size: 18px;
+        white-space: nowrap;
+        text-overflow: ellipsis;
+      }
+      .title::before {
+        display: none;
+      }
+      .desc {
+        width: 100%;
+        margin-top: 10px;
+        overflow: hidden;
+        color: #666;
+        font-weight: 400;
+        font-size: 12px;
+        white-space: nowrap;
+        text-overflow: ellipsis;
+      }
+    }
+
+    .container {
+      display: flex;
+      width: 100%;
+      height: 80px;
+      margin-top: 10px;
+
+      .server,
+      .procotol {
+        width: calc(50% - 20px);
+        margin-right: 10px;
+        .subTitle {
+          width: 100%;
+          overflow: hidden;
+          color: rgba(0, 0, 0, 0.75);
+          font-size: 12px;
+          white-space: nowrap;
+          text-overflow: ellipsis;
+        }
+        p {
+          width: 100%;
+          overflow: hidden;
+          white-space: nowrap;
+          text-overflow: ellipsis;
+        }
+      }
+    }
+  }
+}
+
+:global {
+  .ant-pagination-item {
+    display: none;
+  }
+}

+ 59 - 0
src/components/ProTableCard/CardItems/AccessConfig/index.tsx

@@ -0,0 +1,59 @@
+import React from 'react';
+import { StatusColorEnum } from '@/components/BadgeStatus';
+import { TableCard } from '@/components';
+import '@/style/common.less';
+import { Badge } from 'antd';
+import type { AccessItem } from '@/pages/link/AccessConfig/typings';
+import './index.less';
+
+export interface AccessConfigCardProps extends AccessItem {
+  detail?: React.ReactNode;
+  actions?: React.ReactNode[];
+  avatarSize?: number;
+}
+
+const defaultImage = require('/public/images/device-access.png');
+
+export default (props: AccessConfigCardProps) => {
+  return (
+    <TableCard
+      showMask={false}
+      actions={props.actions}
+      status={props.state.value}
+      statusText={props.state.text}
+      statusNames={{
+        enabled: StatusColorEnum.processing,
+        disabled: StatusColorEnum.error,
+      }}
+      contentClassName={props.state.value === 'disabled' ? 'tableCardDisabled' : 'tableCardEnabled'}
+    >
+      <div className="context-access">
+        <div>
+          <img width={88} height={88} src={defaultImage} alt={''} />
+        </div>
+        <div className="card">
+          <div className="header">
+            <div className="title">{props.name || '--'}</div>
+            <div className="desc">{props.description || '--'}</div>
+          </div>
+          <div className="container">
+            <div className="server">
+              <div className="subTitle">{props?.channelInfo?.name || '--'}</div>
+              <div style={{ width: '100%' }}>
+                {props.channelInfo?.addresses.map((i: any, index: number) => (
+                  <p key={i.address + `_address${index}`}>
+                    <Badge color={i.health === -1 ? 'red' : 'green'} text={i.address} />
+                  </p>
+                ))}
+              </div>
+            </div>
+            <div className="procotol">
+              <div className="subTitle">{props?.protocolDetail?.name || '--'}</div>
+              <p>{props.protocolDetail?.description || '--'}</p>
+            </div>
+          </div>
+        </div>
+      </div>
+    </TableCard>
+  );
+};

+ 4 - 2
src/components/ProTableCard/TableCard.tsx

@@ -1,7 +1,8 @@
 import React, { useState } from 'react';
 import classNames from 'classnames';
 import { BadgeStatus } from '@/components';
-import { StatusColorEnum, StatusColorType } from '@/components/BadgeStatus';
+import { StatusColorEnum } from '@/components/BadgeStatus';
+import type { StatusColorType } from '@/components/BadgeStatus';
 import './index.less';
 
 export interface TableCardProps {
@@ -15,6 +16,7 @@ export interface TableCardProps {
   statusNames?: Record<string | number, StatusColorType>;
   children?: React.ReactNode;
   actions?: React.ReactNode[];
+  contentClassName?: string;
 }
 
 function getAction(actions: React.ReactNode[]) {
@@ -72,7 +74,7 @@ export default (props: TableCardProps) => {
     <div className={classNames('iot-card', { hover: maskShow }, props.className)}>
       <div className={'card-warp'}>
         <div
-          className={'card-content'}
+          className={classNames('card-content', props.contentClassName)}
           onMouseEnter={() => {
             setMaskShow(true);
           }}

+ 117 - 0
src/pages/device/Instance/Detail/Config/Edit.tsx

@@ -0,0 +1,117 @@
+import { createForm } from '@formily/core';
+import { createSchemaField } from '@formily/react';
+import { InstanceModel, service } from '@/pages/device/Instance';
+import type { ISchema } from '@formily/json-schema';
+import { Form, FormGrid, FormItem, Input, Password, PreviewText } from '@formily/antd';
+import { Button, Drawer, message, Space } from 'antd';
+import { useParams } from 'umi';
+
+const componentMap = {
+  string: 'Input',
+  password: 'Password',
+};
+
+interface Props {
+  close: () => void;
+  metadata: any[];
+}
+
+const Edit = (props: Props) => {
+  const { metadata } = props;
+  const params = useParams<{ id: string }>();
+  const id = InstanceModel.detail?.id || params?.id;
+
+  const form = createForm({
+    validateFirst: true,
+    initialValues: InstanceModel.detail?.configuration,
+  });
+
+  const SchemaField = createSchemaField({
+    components: {
+      FormItem,
+      Input,
+      Password,
+      FormGrid,
+      PreviewText,
+    },
+  });
+
+  const configToSchema = (data: any[]) => {
+    const config = {};
+    data.forEach((item) => {
+      config[item.property] = {
+        type: 'string',
+        title: item.name,
+        'x-decorator': 'FormItem',
+        'x-component': componentMap[item.type.type],
+        'x-decorator-props': {
+          tooltip: item.description,
+        },
+      };
+    });
+    return config;
+  };
+
+  const renderConfigCard = () => {
+    return metadata?.map((item: any) => {
+      const itemSchema: ISchema = {
+        type: 'object',
+        properties: {
+          grid: {
+            type: 'void',
+            'x-component': 'FormGrid',
+            'x-component-props': {
+              minColumns: [1],
+              maxColumns: [1],
+            },
+            properties: configToSchema(item.properties),
+          },
+        },
+      };
+
+      return (
+        <>
+          <PreviewText.Placeholder value="-">
+            <Form form={form} layout="vertical">
+              <SchemaField schema={itemSchema} />
+            </Form>
+          </PreviewText.Placeholder>
+        </>
+      );
+    });
+  };
+  return (
+    <Drawer
+      title="编辑配置"
+      placement="right"
+      onClose={() => {
+        props.close();
+      }}
+      visible
+      extra={
+        <Space>
+          <Button
+            type="primary"
+            onClick={async () => {
+              const values = (await form.submit()) as any;
+              const resp = await service.modify(id || '', {
+                id,
+                configuration: { ...values },
+              });
+              if (resp.status === 200) {
+                message.success('操作成功!');
+                props.close();
+              }
+            }}
+          >
+            保存
+          </Button>
+        </Space>
+      }
+    >
+      {renderConfigCard()}
+    </Drawer>
+  );
+};
+
+export default Edit;

+ 136 - 150
src/pages/device/Instance/Detail/Config/index.tsx

@@ -1,19 +1,15 @@
-import { Card, Divider, Empty, message, Popconfirm, Space, Tooltip } from 'antd';
+import { Button, Descriptions, message, Popconfirm, Space, Tooltip } from 'antd';
 import { InstanceModel, service } from '@/pages/device/Instance';
 import { useEffect, useState } from 'react';
-import { createSchemaField } from '@formily/react';
-import type { ConfigMetadata, ConfigProperty } from '@/pages/device/Product/typings';
-import type { ISchema } from '@formily/json-schema';
-import { Form, FormGrid, FormItem, FormLayout, Input, Password, PreviewText } from '@formily/antd';
-import { createForm } from '@formily/core';
+import type { ConfigMetadata } from '@/pages/device/Product/typings';
 import { history, useParams } from 'umi';
-import Tags from '@/pages/device/Instance/Detail/Config/Tags';
-import Icon from '@ant-design/icons';
-
-const componentMap = {
-  string: 'Input',
-  password: 'Password',
-};
+import {
+  CheckOutlined,
+  EditOutlined,
+  QuestionCircleOutlined,
+  UndoOutlined,
+} from '@ant-design/icons';
+import Edit from './Edit';
 
 const Config = () => {
   const params = useParams<{ id: string }>();
@@ -29,13 +25,7 @@ const Config = () => {
   }, []);
 
   const [metadata, setMetadata] = useState<ConfigMetadata[]>([]);
-  const [state, setState] = useState<boolean>(false);
-
-  const form = createForm({
-    validateFirst: true,
-    readPretty: state,
-    initialValues: InstanceModel.detail?.configuration,
-  });
+  const [visible, setVisible] = useState<boolean>(false);
 
   const id = InstanceModel.detail?.id || params?.id;
 
@@ -52,146 +42,142 @@ const Config = () => {
       service.getConfigMetadata(id).then((config) => {
         setMetadata(config?.result);
       });
-      setState(
-        !!(
-          InstanceModel.detail?.configuration &&
-          Object.keys(InstanceModel.detail?.configuration).length > 0
-        ),
-      );
     }
-
-    return () => {};
   }, [id]);
 
-  const SchemaField = createSchemaField({
-    components: {
-      FormItem,
-      Input,
-      Password,
-      FormGrid,
-      PreviewText,
-    },
-  });
-
-  const configToSchema = (data: ConfigProperty[]) => {
-    const config = {};
-    data.forEach((item) => {
-      config[item.property] = {
-        type: 'string',
-        title: item.name,
-        'x-decorator': 'FormItem',
-        'x-component': componentMap[item.type.type],
-        'x-decorator-props': {
-          tooltip: item.description,
-        },
-      };
-    });
-    return config;
+  const isExit = (property: string) => {
+    return (
+      InstanceModel.detail?.cachedConfiguration &&
+      InstanceModel.detail?.cachedConfiguration[property] !== undefined &&
+      InstanceModel.detail?.configuration &&
+      InstanceModel.detail?.configuration[property] !==
+        InstanceModel.detail?.cachedConfiguration[property]
+    );
   };
 
-  const renderConfigCard = () => {
-    return metadata ? (
-      metadata?.map((item) => {
-        const itemSchema: ISchema = {
-          type: 'object',
-          properties: {
-            grid: {
-              type: 'void',
-              'x-component': 'FormGrid',
-              'x-component-props': {
-                minColumns: [2],
-                maxColumns: [2],
-              },
-              properties: configToSchema(item.properties),
-            },
-          },
-        };
-
+  const renderComponent = (item: any) => {
+    if (InstanceModel.detail?.configuration) {
+      const config = InstanceModel.detail?.configuration;
+      if (item.type.type === 'password' && config[item.property]?.length > 0) {
+        return '••••••';
+      }
+      if (isExit(item.property)) {
         return (
-          <>
-            <Divider />
-            <Card
-              title={item.name}
-              extra={
-                <Space>
-                  <a
-                    onClick={async () => {
-                      if (!state) {
-                        const values = (await form.submit()) as any;
-                        const resp = await service.modify(id || '', {
-                          id,
-                          configuration: { ...values },
-                        });
-                        if (resp.status === 200) {
-                          getDetail();
-                        }
-                      }
-                      setState(!state);
-                    }}
-                  >
-                    {state ? '编辑' : '保存'}
-                  </a>
-                  {InstanceModel.detail.state?.value !== 'notActive' && (
-                    <Popconfirm
-                      title="确认重新应用该配置?"
-                      onConfirm={async () => {
-                        const resp = await service.deployDevice(id || '');
-                        if (resp.status === 200) {
-                          message.success('操作成功');
-                          getDetail();
-                        }
-                      }}
-                    >
-                      <a>应用配置</a>
-                      <Tooltip title="修改配置后需重新应用后才能生效。">
-                        <Icon type="question-circle-o" />
-                      </Tooltip>
-                    </Popconfirm>
-                  )}
-                  {InstanceModel.detail?.aloneConfiguration && (
-                    <Popconfirm
-                      title="确认恢复默认配置?"
-                      onConfirm={async () => {
-                        const resp = await service.configurationReset(id || '');
-                        if (resp.status === 200) {
-                          message.success('恢复默认配置成功');
-                          getDetail();
-                        }
-                      }}
-                    >
-                      <a>恢复默认</a>
-                      <Tooltip
-                        title={`该设备单独编辑过配置信息,点击此将恢复成默认的配置信息,请谨慎操作。`}
-                      >
-                        <Icon type="question-circle-o" />
-                      </Tooltip>
-                    </Popconfirm>
-                  )}
-                </Space>
-              }
-            >
-              <PreviewText.Placeholder value="-">
-                <Form form={form}>
-                  <FormLayout labelCol={6} wrapperCol={16}>
-                    <SchemaField schema={itemSchema} />
-                  </FormLayout>
-                </Form>
-              </PreviewText.Placeholder>
-            </Card>
-          </>
+          <div>
+            <span style={{ marginRight: '10px' }}>{config[item.property]}</span>
+            <Tooltip title={`有效值:${config[item.property]}`}>
+              <QuestionCircleOutlined />
+            </Tooltip>
+          </div>
         );
-      })
-    ) : (
-      <Empty />
-    );
+      } else {
+        return <span>{config[item.property]}</span>;
+      }
+    } else {
+      return '--';
+    }
   };
 
   return (
-    <>
-      {renderConfigCard()}
-      <Divider />
-      <Tags />
-    </>
+    <div style={{ width: '100%', marginTop: '20px' }} className="config">
+      <Descriptions
+        layout="vertical"
+        title={[
+          <span key={1}>配置</span>,
+          <Space key={2}>
+            <Button
+              type="link"
+              onClick={async () => {
+                setVisible(true);
+              }}
+            >
+              <EditOutlined />
+              编辑
+            </Button>
+            {InstanceModel.detail.state?.value !== 'notActive' && (
+              <Popconfirm
+                title="确认重新应用该配置?"
+                onConfirm={async () => {
+                  const resp = await service.deployDevice(id || '');
+                  if (resp.status === 200) {
+                    message.success('操作成功');
+                    getDetail();
+                  }
+                }}
+              >
+                <Button type="link">
+                  <CheckOutlined />
+                  应用配置
+                </Button>
+                <Tooltip title="修改配置后需重新应用后才能生效。">
+                  <QuestionCircleOutlined />
+                </Tooltip>
+              </Popconfirm>
+            )}
+            {InstanceModel.detail?.aloneConfiguration && (
+              <Popconfirm
+                title="确认恢复默认配置?"
+                onConfirm={async () => {
+                  const resp = await service.configurationReset(id || '');
+                  if (resp.status === 200) {
+                    message.success('恢复默认配置成功');
+                    getDetail();
+                  }
+                }}
+              >
+                <Button type="link">
+                  <UndoOutlined />
+                  恢复默认
+                </Button>
+                <Tooltip
+                  title={`该设备单独编辑过配置信息,点击此将恢复成默认的配置信息,请谨慎操作。`}
+                >
+                  <QuestionCircleOutlined />
+                </Tooltip>
+              </Popconfirm>
+            )}
+          </Space>,
+        ]}
+      >
+        {(metadata || []).map((i) => (
+          <Descriptions.Item key={i.name} label={<h4>{i.name}</h4>} span={3}>
+            <div style={{ width: '100%' }}>
+              <Descriptions column={2} bordered size="small">
+                {(i?.properties || []).map((item: any) => (
+                  <Descriptions.Item
+                    span={1}
+                    label={
+                      item.description ? (
+                        <div>
+                          <span style={{ marginRight: '10px' }}>{item.name}</span>
+                          <Tooltip title={item.description}>
+                            <QuestionCircleOutlined />
+                          </Tooltip>
+                        </div>
+                      ) : (
+                        item.name
+                      )
+                    }
+                    key={item.property}
+                  >
+                    {renderComponent(item)}
+                  </Descriptions.Item>
+                ))}
+              </Descriptions>
+            </div>
+          </Descriptions.Item>
+        ))}
+      </Descriptions>
+      {visible && (
+        <Edit
+          metadata={metadata || []}
+          close={() => {
+            setVisible(false);
+            getDetail();
+          }}
+        />
+      )}
+    </div>
   );
 };
 

+ 25 - 15
src/pages/device/Instance/Detail/Info/index.tsx

@@ -1,4 +1,4 @@
-import { Card, Descriptions } from 'antd';
+import { Button, Card, Descriptions } from 'antd';
 import { InstanceModel } from '@/pages/device/Instance';
 import moment from 'moment';
 import { observer } from '@formily/react';
@@ -7,6 +7,8 @@ import Config from '@/pages/device/Instance/Detail/Config';
 import Save from '../../Save';
 import { useState } from 'react';
 import type { DeviceInstance } from '../../typings';
+import { EditOutlined } from '@ant-design/icons';
+import Tags from '@/pages/device/Instance/Detail/Tags';
 
 const Info = observer(() => {
   const intl = useIntl();
@@ -14,19 +16,25 @@ const Info = observer(() => {
 
   return (
     <>
-      <Card
-        title={'设备信息'}
-        extra={
-          <a
-            onClick={() => {
-              setVisible(true);
-            }}
-          >
-            编辑
-          </a>
-        }
-      >
-        <Descriptions size="small" column={3} bordered>
+      <Card>
+        <Descriptions
+          size="small"
+          column={3}
+          bordered
+          title={[
+            <span key={1}>设备信息</span>,
+            <Button
+              key={2}
+              type={'link'}
+              onClick={() => {
+                setVisible(true);
+              }}
+            >
+              <EditOutlined />
+              编辑
+            </Button>,
+          ]}
+        >
           <Descriptions.Item
             label={intl.formatMessage({
               id: 'pages.table.deviceId',
@@ -102,8 +110,10 @@ const Info = observer(() => {
             {InstanceModel.detail?.description}
           </Descriptions.Item>
         </Descriptions>
+        {InstanceModel.detail?.configuration &&
+          Object.keys(InstanceModel.detail?.configuration).length > 0 && <Config />}
+        {InstanceModel.detail?.tags && InstanceModel.detail?.tags.length > 0 && <Tags />}
       </Card>
-      <Config />
       <Save
         model={'edit'}
         data={{ ...InstanceModel?.detail, describe: InstanceModel?.detail?.description || '' }}

+ 69 - 52
src/pages/device/Instance/Detail/Config/Tags/index.tsx

@@ -1,31 +1,18 @@
-import { createSchemaField, FormProvider } from '@formily/react';
-import { Editable, FormItem, Input, ArrayTable } from '@formily/antd';
 import { createForm } from '@formily/core';
-import { Card, message } from 'antd';
-import { useIntl } from '@@/plugin-locale/localeExports';
+import { createSchemaField, FormProvider } from '@formily/react';
 import { InstanceModel, service } from '@/pages/device/Instance';
-import { useEffect, useState } from 'react';
+import { ArrayTable, FormItem, Input } from '@formily/antd';
+import { message, Modal } from 'antd';
+import { useIntl } from 'umi';
 
-const SchemaField = createSchemaField({
-  components: {
-    FormItem,
-    Editable,
-    Input,
-    ArrayTable,
-  },
-});
+interface Props {
+  close: () => void;
+  tags: any[];
+}
 
-const Tags = () => {
+const Edit = (props: Props) => {
+  const { tags } = props;
   const intl = useIntl();
-  const [tags, setTags] = useState<any[]>([]);
-
-  const tag = InstanceModel.detail?.tags;
-
-  useEffect(() => {
-    if (tag) {
-      setTags([...tag] || []);
-    }
-  }, [tag]);
 
   const form = createForm({
     initialValues: {
@@ -33,6 +20,14 @@ const Tags = () => {
     },
   });
 
+  const SchemaField = createSchemaField({
+    components: {
+      FormItem,
+      Input,
+      ArrayTable,
+    },
+  });
+
   const schema = {
     type: 'object',
     properties: {
@@ -56,7 +51,7 @@ const Tags = () => {
                   type: 'string',
                   'x-decorator': 'FormItem',
                   'x-component': 'Input',
-                  'x-disabled': true,
+                  // 'x-disabled': true
                 },
               },
             },
@@ -73,7 +68,7 @@ const Tags = () => {
               properties: {
                 name: {
                   type: 'string',
-                  'x-decorator': 'Editable',
+                  'x-decorator': 'FormItem',
                   'x-component': 'Input',
                 },
               },
@@ -91,47 +86,69 @@ const Tags = () => {
               properties: {
                 value: {
                   type: 'string',
-                  'x-decorator': 'Editable',
+                  'x-decorator': 'FormItem',
                   'x-component': 'Input',
                 },
               },
             },
+            column4: {
+              type: 'void',
+              'x-component': 'ArrayTable.Column',
+              'x-component-props': {
+                width: 100,
+                title: '操作',
+                dataIndex: 'operations',
+              },
+              properties: {
+                item: {
+                  type: 'void',
+                  'x-component': 'FormItem',
+                  properties: {
+                    remove: {
+                      type: 'void',
+                      'x-component': 'ArrayTable.Remove',
+                    },
+                  },
+                },
+              },
+            },
+          },
+        },
+        properties: {
+          add: {
+            type: 'void',
+            'x-component': 'ArrayTable.Addition',
+            title: '添加',
           },
         },
       },
     },
   };
+
   return (
-    <Card
-      title={intl.formatMessage({
-        id: 'pages.device.instanceDetail.tags',
-        defaultMessage: '标签',
-      })}
-      extra={
-        <a
-          onClick={async () => {
-            const values = (await form.submit()) as any;
-            if (values?.tags) {
-              const resp = await service.saveTags(InstanceModel.detail?.id || '', values.tags);
-              if (resp.status === 200) {
-                InstanceModel.detail = { ...InstanceModel.detail, tags: values.tags };
-                message.success('操作成功!');
-              }
-            }
-          }}
-        >
-          {intl.formatMessage({
-            id: 'pages.device.instanceDetail.save',
-            defaultMessage: '保存',
-          })}
-        </a>
-      }
+    <Modal
+      title="编辑标签"
+      onCancel={() => {
+        props.close();
+      }}
+      visible
+      width={1000}
+      onOk={async () => {
+        const values: any = (await form.submit()) as any;
+        const list = (values?.tags || []).filter((item: any) => item?.id);
+        const resp = await service.saveTags(InstanceModel.detail?.id || '', list);
+        if (resp.status === 200) {
+          InstanceModel.detail = { ...InstanceModel.detail, tags: values.tags };
+          message.success('操作成功!');
+          props.close();
+        }
+      }}
     >
       <FormProvider form={form}>
         <SchemaField schema={schema} />
       </FormProvider>
-    </Card>
+    </Modal>
   );
 };
 
-export default Tags;
+export default Edit;

+ 63 - 0
src/pages/device/Instance/Detail/Tags/index.tsx

@@ -0,0 +1,63 @@
+import { Button, Descriptions } from 'antd';
+import { useIntl } from '@@/plugin-locale/localeExports';
+import { InstanceModel } from '@/pages/device/Instance';
+import { useEffect, useState } from 'react';
+import { EditOutlined } from '@ant-design/icons';
+import Edit from './Edit';
+
+const Tags = () => {
+  const intl = useIntl();
+  const [tags, setTags] = useState<any[]>([]);
+  const [visible, setVisible] = useState<boolean>(false);
+
+  const tag = InstanceModel.detail?.tags;
+
+  useEffect(() => {
+    if (tag) {
+      setTags([...tag] || []);
+    }
+  }, [tag]);
+
+  return (
+    <div style={{ width: '100%', marginTop: '20px' }}>
+      <Descriptions
+        style={{ marginBottom: 20 }}
+        bordered
+        size="small"
+        title={
+          <span>
+            {intl.formatMessage({
+              id: 'pages.device.instanceDetail.tags',
+              defaultMessage: '标签',
+            })}
+            <Button
+              type="link"
+              onClick={() => {
+                setVisible(true);
+              }}
+            >
+              <EditOutlined />
+              编辑
+            </Button>
+          </span>
+        }
+      >
+        {(tags || [])?.map((item: any) => (
+          <Descriptions.Item label={`${item.name}(${item.key})`} key={item.key}>
+            {item.value || '--'}
+          </Descriptions.Item>
+        ))}
+      </Descriptions>
+      {visible && (
+        <Edit
+          close={() => {
+            setVisible(false);
+          }}
+          tags={tags}
+        />
+      )}
+    </div>
+  );
+};
+
+export default Tags;

+ 40 - 25
src/pages/device/Product/Detail/Access/index.tsx

@@ -12,6 +12,7 @@ import type { ConfigProperty } from '@/pages/device/Product/typings';
 import { createSchemaField } from '@formily/react';
 import { createForm } from '@formily/core';
 import { QuestionCircleOutlined } from '@ant-design/icons';
+import TitleComponent from '@/components/TitleComponent';
 
 const componentMap = {
   string: 'Input',
@@ -286,6 +287,24 @@ const Access = () => {
                 minColumns: 1,
                 columnGap: 48,
               },
+              title: (
+                <TitleComponent
+                  data={
+                    <span>
+                      {item.name}
+                      <Tooltip title="此配置来自于该产品接入方式所选择的协议">
+                        <QuestionCircleOutlined />
+                      </Tooltip>
+                    </span>
+                  }
+                />
+              ),
+              'x-decorator': 'FormItem',
+              'x-decorator-props': {
+                gridSpan: 1,
+                labelAlign: 'left',
+                layout: 'vertical',
+              },
               properties: configToSchema(item.properties),
             },
           },
@@ -345,20 +364,24 @@ const Access = () => {
           <Col span={12}>
             <div className={styles.config}>
               <div className={styles.item}>
-                <div className={styles.title}>
-                  接入方式
-                  <Button
-                    size="small"
-                    type="primary"
-                    ghost
-                    style={{ marginLeft: 20 }}
-                    onClick={() => {
-                      setConfigVisible(true);
-                    }}
-                  >
-                    更换
-                  </Button>
-                </div>
+                <TitleComponent
+                  data={
+                    <span>
+                      接入方式
+                      <Button
+                        size="small"
+                        type="primary"
+                        ghost
+                        style={{ marginLeft: 20 }}
+                        onClick={() => {
+                          setConfigVisible(true);
+                        }}
+                      >
+                        更换
+                      </Button>
+                    </span>
+                  }
+                />
                 <div className={styles.context}>
                   {providers.find((i) => i.id === access?.provider)?.name || '--'}
                 </div>
@@ -372,7 +395,7 @@ const Access = () => {
               </div>
 
               <div className={styles.item}>
-                <div className={styles.title}>消息协议</div>
+                <TitleComponent data={'消息协议'} />
                 <div className={styles.context}>{access?.protocolDetail?.name || '--'}</div>
                 {config?.document && (
                   <div className={styles.context}>
@@ -382,7 +405,7 @@ const Access = () => {
               </div>
 
               <div className={styles.item}>
-                <div className={styles.title}>连接信息</div>
+                <TitleComponent data={'连接信息'} />
                 {(networkList.find((i) => i.id === access?.channelId)?.addresses || []).length > 0
                   ? (networkList.find((i) => i.id === access?.channelId)?.addresses || []).map(
                       (item: any) => (
@@ -398,15 +421,7 @@ const Access = () => {
                   : '暂无连接信息'}
               </div>
 
-              <div className={styles.item}>
-                <div className={styles.title}>
-                  认证配置
-                  <Tooltip title="此配置来自于该产品接入方式所选择的协议">
-                    <QuestionCircleOutlined />
-                  </Tooltip>
-                </div>
-                {renderConfigCard()}
-              </div>
+              <div className={styles.item}>{renderConfigCard()}</div>
             </div>
           </Col>
           <Col span={12}>

+ 40 - 72
src/pages/link/AccessConfig/Detail/Access/index.tsx

@@ -16,7 +16,7 @@ import { useEffect, useState } from 'react';
 import styles from './index.less';
 import { service } from '@/pages/link/AccessConfig';
 import encodeQuery from '@/utils/encodeQuery';
-import { useHistory, useLocation } from 'umi';
+import { useHistory } from 'umi';
 import ReactMarkdown from 'react-markdown';
 import { getMenuPathByCode, MENUS_CODE } from '@/utils/menu';
 import { ExclamationCircleFilled } from '@ant-design/icons';
@@ -24,12 +24,9 @@ import { ExclamationCircleFilled } from '@ant-design/icons';
 interface Props {
   change: () => void;
   data: any;
+  provider: any;
 }
 
-type LocationType = {
-  id?: string;
-};
-
 const Access = (props: Props) => {
   const [form] = Form.useForm();
 
@@ -38,11 +35,9 @@ const Access = (props: Props) => {
   const [current, setCurrent] = useState<number>(0);
   const [networkList, setNetworkList] = useState<any[]>([]);
   const [procotolList, setProcotolList] = useState<any[]>([]);
-  const [access, setAccess] = useState<any>({});
   const [procotolCurrent, setProcotolCurrent] = useState<string>('');
   const [networkCurrent, setNetworkCurrent] = useState<string>('');
   const [config, setConfig] = useState<any>();
-  const [providers, setProviders] = useState<any[]>([]);
 
   const MetworkTypeMapping = new Map();
   MetworkTypeMapping.set('websocket-server', 'WEB_SOCKET_SERVER');
@@ -78,48 +73,32 @@ const Access = (props: Props) => {
     });
   };
 
-  const queryProviders = () => {
-    service.getProviders().then((resp) => {
-      if (resp.status === 200) {
-        setProviders(resp.result);
-      }
-    });
-  };
-
   useEffect(() => {
-    if (props.data?.id) {
-      queryNetworkList(props.data?.id);
+    if (props.provider?.id) {
+      queryNetworkList(props.provider?.id);
       setCurrent(0);
     }
-  }, [props.data]);
-
-  const location = useLocation<LocationType>();
-
-  const params = new URLSearchParams(location.search);
+  }, [props.provider]);
 
   useEffect(() => {
-    if (params.get('id')) {
-      service.detail(params.get('id') || '').then((resp) => {
-        setAccess(resp.result);
-        setProcotolCurrent(resp.result?.protocol);
-        setNetworkCurrent(resp.result?.channelId);
-        form.setFieldsValue({
-          name: resp.result?.name,
-          description: resp.result?.description,
-        });
-        queryProviders();
-        setCurrent(0);
-        queryNetworkList(resp.result?.provider);
+    if (props.data?.id) {
+      setProcotolCurrent(props.data?.protocol);
+      setNetworkCurrent(props.data?.channelId);
+      form.setFieldsValue({
+        name: props.data?.name,
+        description: props.data?.description,
       });
+      setCurrent(0);
+      queryNetworkList(props.data?.provider);
     }
-  }, []);
+  }, [props.data]);
 
   const next = () => {
     if (current === 0) {
       if (!networkCurrent) {
         message.error('请选择网络组件!');
       } else {
-        queryProcotolList(props.data?.id || access?.provider);
+        queryProcotolList(props.provider?.id);
         setCurrent(current + 1);
       }
     }
@@ -128,7 +107,7 @@ const Access = (props: Props) => {
         message.error('请选择消息协议!');
       } else {
         service
-          .getConfigView(procotolCurrent, ProcotoleMapping.get(props.data?.id || access?.provider))
+          .getConfigView(procotolCurrent, ProcotoleMapping.get(props.provider?.id))
           .then((resp) => {
             if (resp.status === 200) {
               setConfig(resp.result);
@@ -301,7 +280,7 @@ const Access = (props: Props) => {
                 placeholder="请输入名称"
                 onSearch={(value: string) => {
                   queryNetworkList(
-                    props.data?.id || access?.provider,
+                    props.provider?.id,
                     encodeQuery({
                       terms: {
                         name$LIKE: `%${value}%`,
@@ -318,7 +297,7 @@ const Access = (props: Props) => {
                   const tab: any = window.open(`${origin}/#${url}`);
                   tab!.onTabSaveSuccess = (value: any) => {
                     if (value.status === 200) {
-                      queryNetworkList(props.data?.id || access?.provider);
+                      queryNetworkList(props.provider?.id);
                     }
                   };
                 }}
@@ -377,7 +356,7 @@ const Access = (props: Props) => {
                         const tab: any = window.open(`${origin}/#${url}`);
                         tab!.onTabSaveSuccess = (value: any) => {
                           if (value.status === 200) {
-                            queryNetworkList(props.data?.id || access?.provider);
+                            queryNetworkList(props.provider?.id);
                           }
                         };
                       }}
@@ -403,7 +382,7 @@ const Access = (props: Props) => {
                 placeholder="请输入名称"
                 onSearch={(value: string) => {
                   queryProcotolList(
-                    props.data?.id || access?.provider,
+                    props.provider?.id,
                     encodeQuery({
                       terms: {
                         name$LIKE: `%${value}%`,
@@ -416,10 +395,11 @@ const Access = (props: Props) => {
               <Button
                 type="primary"
                 onClick={() => {
-                  const tab: any = window.open(`${origin}/#/link/Protocol?save=true`);
+                  const url = getMenuPathByCode(MENUS_CODE['link/Protocol?save=true']);
+                  const tab: any = window.open(`${origin}/#${url}`);
                   tab!.onTabSaveSuccess = (value: any) => {
                     if (value) {
-                      queryProcotolList(props.data?.id || access?.provider);
+                      queryProcotolList(props.provider?.id);
                     }
                   };
                 }}
@@ -436,7 +416,7 @@ const Access = (props: Props) => {
                       style={{
                         width: '100%',
                         borderColor:
-                          networkCurrent === item.id ? 'var(--ant-primary-color-active)' : '',
+                          procotolCurrent === item.id ? 'var(--ant-primary-color-active)' : '',
                       }}
                       hoverable
                       onClick={() => {
@@ -458,10 +438,11 @@ const Access = (props: Props) => {
                     暂无数据
                     <a
                       onClick={() => {
-                        const tab: any = window.open(`${origin}/#/link/Protocol?save=true`);
+                        const url = getMenuPathByCode(MENUS_CODE['link/Protocol?save=true']);
+                        const tab: any = window.open(`${origin}/#${url}`);
                         tab!.onTabSaveSuccess = (value: any) => {
                           if (value) {
-                            queryProcotolList(props.data?.id || access?.provider);
+                            queryProcotolList(props.provider?.id);
                           }
                         };
                       }}
@@ -502,14 +483,14 @@ const Access = (props: Props) => {
                       try {
                         const values = await form.validateFields();
                         // 编辑还是保存
-                        if (!params.get('id')) {
+                        if (!props.data?.id) {
                           service
                             .save({
                               name: values.name,
                               description: values.description,
-                              provider: props.data.id,
+                              provider: props.provider.id,
                               protocol: procotolCurrent,
-                              transport: ProcotoleMapping.get(props.data.id),
+                              transport: ProcotoleMapping.get(props.provider.id),
                               channel: 'network', // 网络组件
                               channelId: networkCurrent,
                             })
@@ -526,12 +507,10 @@ const Access = (props: Props) => {
                         } else {
                           service
                             .update({
-                              id: access?.id,
+                              ...props.data,
                               name: values.name,
                               description: values.description,
-                              provider: access?.provider,
                               protocol: procotolCurrent,
-                              transport: access?.transport,
                               channel: 'network', // 网络组件
                               channelId: networkCurrent,
                             })
@@ -560,30 +539,19 @@ const Access = (props: Props) => {
               <div className={styles.config}>
                 <div className={styles.item}>
                   <div className={styles.title}>接入方式</div>
-                  <div className={styles.context}>
-                    {props.data?.name ||
-                      providers.find((i) => i.id === access?.provider)?.name ||
-                      '--'}
-                  </div>
-                  <div className={styles.context}>
-                    {((props.data?.description ||
-                      providers.find((i) => i.id === access?.provider)?.description) && (
-                      <span>
-                        {props.data?.description ||
-                          providers.find((i) => i.id === access?.provider)?.description}
-                      </span>
-                    )) ||
-                      '--'}
-                  </div>
+                  <div className={styles.context}>{props.provider?.name || '--'}</div>
+                  <div className={styles.context}>{props.provider?.description || '--'}</div>
                 </div>
                 <div className={styles.item}>
                   <div className={styles.title}>消息协议</div>
                   <div className={styles.context}>
                     {procotolList.find((i) => i.id === procotolCurrent)?.name || '--'}
                   </div>
-                  <div className={styles.context}>
-                    {config?.document ? <ReactMarkdown>{config?.document}</ReactMarkdown> : '--'}
-                  </div>
+                  {config?.document && (
+                    <div className={styles.context}>
+                      {<ReactMarkdown>{config?.document}</ReactMarkdown> || '--'}
+                    </div>
+                  )}
                 </div>
                 <div className={styles.item}>
                   <div className={styles.title}>网络组件</div>
@@ -604,8 +572,8 @@ const Access = (props: Props) => {
                 {config?.routes && config?.routes?.length > 0 && (
                   <div className={styles.item}>
                     <div style={{ fontWeight: '600', marginBottom: 10 }}>
-                      {access?.provider === 'mqtt-server-gateway' ||
-                      access?.provider === 'mqtt-client-gateway'
+                      {props.data?.provider === 'mqtt-server-gateway' ||
+                      props.data?.provider === 'mqtt-client-gateway'
                         ? 'topic'
                         : 'URL信息'}
                     </div>

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

@@ -56,6 +56,7 @@ const Detail = () => {
         return (
           <Access
             data={data}
+            provider={provider}
             change={() => {
               setVisible(true);
             }}

+ 10 - 39
src/pages/link/AccessConfig/index.less

@@ -1,43 +1,14 @@
-// .box {
-//   display: flex;
-//   justify-content: space-between;
-// }
-
-// .images {
-//   width: 64px;
-//   height: 64px;
-//   color: white;
-//   font-size: 18px;
-//   line-height: 64px;
-//   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%;
-// }
-
-// .content {
-//   display: flex;
-//   flex-direction: column;
-//   width: calc(100% - 80px);
-// }
-
-// .top {
-//   display: flex;
-//   justify-content: space-between;
-
-//   .left {
-//     display: flex;
-//   }
+.tableCardDisabled {
+  width: 100%;
+  background: url('/images/access-config-diaabled.png') no-repeat;
+  background-size: 100% 100%;
+}
 
-//   .action a {
-//     margin: 0 5px;
-//   }
-// }
+.tableCardEnabled {
+  width: 100%;
+  background: url('/images/access-config-enabled.png') no-repeat;
+  background-size: 100% 100%;
+}
 
 .context {
   display: flex;

+ 5 - 46
src/pages/link/AccessConfig/index.tsx

@@ -1,17 +1,13 @@
-import { TableCard } from '@/components';
 import SearchComponent from '@/components/SearchComponent';
 import { getMenuPathByCode, MENUS_CODE } from '@/utils/menu';
-import { StatusColorEnum } from '@/components/BadgeStatus';
 import { PageContainer } from '@ant-design/pro-layout';
 import type { ProColumns } from '@jetlinks/pro-table';
-import { Badge, Button, Card, Col, Empty, message, Pagination, Popconfirm, Row } from 'antd';
+import { Button, Card, Col, Empty, message, Pagination, Popconfirm, Row } from 'antd';
 import { useEffect, useState } from 'react';
 import { useHistory } from 'umi';
-import styles from './index.less';
 import Service from './service';
 import { CheckCircleOutlined, DeleteOutlined, EditOutlined, StopOutlined } from '@ant-design/icons';
-
-const defaultImage = require('/public/images/device-access.png');
+import AccessConfigCard from '@/components/ProTableCard/CardItems/AccessConfig';
 
 export const service = new Service('gateway/device');
 
@@ -72,7 +68,6 @@ const AccessConfig = () => {
       <Card>
         <SearchComponent
           field={columns}
-          // pattern={'simple'}
           enableSave={false}
           onSearch={(data: any) => {
             const dt = {
@@ -96,8 +91,8 @@ const AccessConfig = () => {
           <Row gutter={[16, 16]} style={{ marginTop: 10 }}>
             {(dataSource?.data || []).map((item: any) => (
               <Col key={item.id} span={12}>
-                <TableCard
-                  showMask={false}
+                <AccessConfigCard
+                  {...item}
                   actions={[
                     <Button
                       key="edit"
@@ -166,43 +161,7 @@ const AccessConfig = () => {
                       </Popconfirm>
                     </Button>,
                   ]}
-                  status={item.state.value}
-                  statusText={item.state.text}
-                  statusNames={{
-                    enabled: StatusColorEnum.processing,
-                    disabled: StatusColorEnum.error,
-                  }}
-                >
-                  <div className={styles.context}>
-                    <div>
-                      <img width={88} height={88} src={defaultImage} alt={''} />
-                    </div>
-                    <div className={styles.card}>
-                      <div className={styles.header}>
-                        <div className={styles.title}>{item.name || '--'}</div>
-                        <div className={styles.desc}>{item.description || '--'}</div>
-                      </div>
-                      <div className={styles.container}>
-                        <div className={styles.server}>
-                          <div className={styles.subTitle}>{item?.channelInfo?.name || '--'}</div>
-                          <div style={{ width: '100%' }}>
-                            {item.channelInfo?.addresses.map((i: any, index: number) => (
-                              <p key={i.address + `_address${index}`}>
-                                <Badge color={i.health === -1 ? 'red' : 'green'} text={i.address} />
-                              </p>
-                            ))}
-                          </div>
-                        </div>
-                        <div className={styles.procotol}>
-                          <div className={styles.subTitle}>
-                            {item?.protocolDetail?.name || '--'}
-                          </div>
-                          <p>{item.protocolDetail?.description || '--'}</p>
-                        </div>
-                      </div>
-                    </div>
-                  </div>
-                </TableCard>
+                />
               </Col>
             ))}
           </Row>

+ 7 - 5
src/pages/media/Cascade/Save/index.tsx

@@ -27,12 +27,14 @@ const Save = () => {
   const id = location?.query?.id || '';
 
   const checkSIP = (_: any, value: { host: string; port: number }) => {
-    if (!value) {
-      return Promise.reject(new Error('请输入SIP'));
-    } else if (Number(value.port) < 1 || Number(value.port) > 65535) {
-      return Promise.reject(new Error('端口请输入1~65535之间的正整数'));
-    } else if (!testIP(value.host)) {
+    if (!value || !value.host) {
+      return Promise.reject(new Error('请输入API HOST'));
+    } else if (value?.host && !testIP(value.host)) {
       return Promise.reject(new Error('请输入正确的IP地址'));
+    } else if (!value?.port) {
+      return Promise.reject(new Error('请输入端口'));
+    } else if ((value?.port && Number(value.port) < 1) || Number(value.port) > 65535) {
+      return Promise.reject(new Error('端口请输入1~65535之间的正整数'));
     }
     return Promise.resolve();
   };

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

@@ -15,7 +15,7 @@ import type { CascadeItem } from '@/pages/media/Cascade/typings';
 import { useIntl } from '@@/plugin-locale/localeExports';
 import SearchComponent from '@/components/SearchComponent';
 import { ProTableCard } from '@/components';
-import CascadeCard from '@/components//ProTableCard/CardItems/cascade';
+import CascadeCard from '@/components/ProTableCard/CardItems/cascade';
 import { getMenuPathByCode, MENUS_CODE } from '@/utils/menu';
 import { useHistory } from 'umi';
 import Service from './service';

+ 32 - 23
src/pages/media/Stream/Detail/index.tsx

@@ -171,13 +171,20 @@ const Detail = () => {
   const checkSIP = (_: any, value: { host: string; port: number }) => {
     if (!value || !value.host) {
       return Promise.reject(new Error('请输入API HOST'));
-    } else if ((value?.port && Number(value.port) < 1) || Number(value.port) > 65535) {
-      return Promise.reject(new Error('端口请输入1~65535之间的正整数'));
     } else if (value?.host && !testIP(value.host)) {
       return Promise.reject(new Error('请输入正确的IP地址'));
+    } else if (!value?.port) {
+      return Promise.reject(new Error('请输入端口'));
+    } else if ((value?.port && Number(value.port) < 1) || Number(value.port) > 65535) {
+      return Promise.reject(new Error('端口请输入1~65535之间的正整数'));
     }
     return Promise.resolve();
   };
+
+  const testPort = (value: any) => {
+    return (value && Number(value) < 1) || Number(value) > 65535;
+  };
+
   const checkRIP = (
     _: any,
     value: {
@@ -189,25 +196,29 @@ const Detail = () => {
   ) => {
     if (!value || !value.rtpIp) {
       return Promise.reject(new Error('请输入RTP IP'));
-    } else if (value.rtpIp && !testIP(value.rtpIp)) {
+    } else if (value?.rtpIp && !testIP(value.rtpIp)) {
       return Promise.reject(new Error('请输入正确的IP地址'));
+    } else if (!value.dynamicRtpPort) {
+      if (value.rtpIp && !testIP(value.rtpIp)) {
+        return Promise.reject(new Error('请输入正确的IP地址'));
+      }
+      if (!value?.rtpPort) {
+        return Promise.reject(new Error('请输入端口'));
+      }
+      if (testPort(value?.rtpPort)) {
+        return Promise.reject(new Error('端口请输入1~65535之间的正整数'));
+      }
     } else if (value.dynamicRtpPort) {
       if (value.dynamicRtpPortRange) {
-        if (value.dynamicRtpPortRange?.[0]) {
-          if (
-            Number(value.dynamicRtpPortRange?.[0]) < 1 ||
-            Number(value.dynamicRtpPortRange?.[0]) > 65535
-          ) {
-            return Promise.reject(new Error('端口请输入1~65535之间的正整数'));
-          }
+        if (!value.dynamicRtpPortRange?.[0]) {
+          return Promise.reject(new Error('请输入起始端口'));
+        } else if (testPort(value.dynamicRtpPortRange?.[0])) {
+          return Promise.reject(new Error('端口请输入1~65535之间的正整数'));
         }
-        if (value.dynamicRtpPortRange?.[1]) {
-          if (
-            Number(value.dynamicRtpPortRange?.[1]) < 1 ||
-            Number(value.dynamicRtpPortRange?.[1]) > 65535
-          ) {
-            return Promise.reject(new Error('端口请输入1~65535之间的正整数'));
-          }
+        if (!value.dynamicRtpPortRange?.[1]) {
+          return Promise.reject(new Error('请输入终止端口'));
+        } else if (testPort(value.dynamicRtpPortRange?.[1])) {
+          return Promise.reject(new Error('端口请输入1~65535之间的正整数'));
         }
         if (
           value.dynamicRtpPortRange?.[0] &&
@@ -216,10 +227,8 @@ const Detail = () => {
         ) {
           return Promise.reject(new Error('终止端口需大于等于起始端'));
         }
-      }
-    } else {
-      if ((value.rtpPort && Number(value.rtpPort) < 1) || Number(value.rtpPort) > 65535) {
-        return Promise.reject(new Error('端口请输入1~65535之间的正整数'));
+      } else if (!value.dynamicRtpPortRange) {
+        return Promise.reject(new Error('请输入端口'));
       }
     }
     return Promise.resolve();
@@ -316,7 +325,7 @@ const Detail = () => {
                   </span>
                 }
                 name="api"
-                rules={[{ validator: checkSIP }]}
+                rules={[{ required: true }, { validator: checkSIP }]}
               >
                 <SipComponent />
               </Form.Item>
@@ -335,7 +344,7 @@ const Detail = () => {
                   </span>
                 }
                 name="rtp"
-                rules={[{ validator: checkRIP }]}
+                rules={[{ required: true }, { validator: checkRIP }]}
               >
                 <RTPComponent />
               </Form.Item>

+ 1 - 0
src/pages/notice/Config/typings.d.ts

@@ -31,5 +31,6 @@ type ConfigMetadata = {
     type: string;
     expands?: Record<string, any>;
   };
+  properties: ConfigProperty[];
   scopes: any[];
 };