فهرست منبع

fix: 修改样式4.1

sun-chaochao 3 سال پیش
والد
کامیت
0ae87516d6
32فایلهای تغییر یافته به همراه2074 افزوده شده و 888 حذف شده
  1. BIN
      public/images/access.png
  2. BIN
      public/images/device-access.png
  3. 52 0
      src/pages/Log/Access/Detail/index.tsx
  4. 136 0
      src/pages/Log/Access/index.tsx
  5. 16 0
      src/pages/Log/Access/typings.d.ts
  6. 35 0
      src/pages/Log/System/Detail/index.tsx
  7. 140 0
      src/pages/Log/System/index.tsx
  8. 14 0
      src/pages/Log/System/typings.d.ts
  9. 28 0
      src/pages/Log/index.tsx
  10. 0 163
      src/pages/device/Instance/Detail/Running/Property/PropertyCard copy.tsx
  11. 12 0
      src/pages/device/Instance/Detail/Running/Property/PropertyCard.less
  12. 18 3
      src/pages/device/Instance/Detail/Running/Property/PropertyCard.tsx
  13. 1 1
      src/pages/device/Instance/Detail/Running/Property/index.tsx
  14. 32 3
      src/pages/device/Instance/Detail/Running/index.tsx
  15. 128 0
      src/pages/device/Product/Detail/Access/AccessConfig/index.less
  16. 35 35
      src/pages/device/Product/Detail/Access/AccessConfig/index.tsx
  17. 14 98
      src/pages/device/Product/Detail/Access/index.less
  18. 208 45
      src/pages/device/Product/Detail/Access/index.tsx
  19. 118 124
      src/pages/device/Product/Detail/BaseInfo/index.tsx
  20. 32 13
      src/pages/device/Product/Detail/index.tsx
  21. 42 17
      src/pages/link/AccessConfig/Detail/Access/index.less
  22. 160 128
      src/pages/link/AccessConfig/Detail/Access/index.tsx
  23. 74 0
      src/pages/link/AccessConfig/Detail/Media/index.less
  24. 384 0
      src/pages/link/AccessConfig/Detail/Media/index.tsx
  25. 14 0
      src/pages/link/AccessConfig/Detail/Provider/index.less
  26. 62 10
      src/pages/link/AccessConfig/Detail/Provider/index.tsx
  27. 29 8
      src/pages/link/AccessConfig/Detail/index.tsx
  28. 87 67
      src/pages/link/AccessConfig/index.less
  29. 144 135
      src/pages/link/AccessConfig/index.tsx
  30. 46 26
      src/pages/media/Stream/index.less
  31. 10 9
      src/pages/media/Stream/index.tsx
  32. 3 3
      src/pages/rule-engine/Instance/index.tsx

BIN
public/images/access.png


BIN
public/images/device-access.png


+ 52 - 0
src/pages/Log/Access/Detail/index.tsx

@@ -0,0 +1,52 @@
+import { Descriptions, Modal } from 'antd';
+import type { AccessLogItem } from '@/pages/Log/Access/typings';
+import { useEffect, useState } from 'react';
+import moment from 'moment';
+
+interface Props {
+  data: Partial<AccessLogItem>;
+  close: () => void;
+}
+
+const Detail = (props: Props) => {
+  const [data, setDada] = useState<Partial<AccessLogItem>>(props.data || {});
+
+  useEffect(() => {
+    setDada(props.data);
+  }, [props.data]);
+
+  return (
+    <Modal title={'详情'} visible onCancel={props.close} onOk={props.close} width={1000}>
+      <Descriptions bordered>
+        <Descriptions.Item label="URL">{data?.url}</Descriptions.Item>
+        <Descriptions.Item label="请求方法" span={2}>
+          {data?.httpMethod}
+        </Descriptions.Item>
+        <Descriptions.Item label="动作">{data?.action}</Descriptions.Item>
+        <Descriptions.Item label="类名" span={2}>
+          {data?.target}
+        </Descriptions.Item>
+        <Descriptions.Item label="方法名">{data?.method}</Descriptions.Item>
+        <Descriptions.Item label="IP" span={2}>
+          {data?.ip}
+        </Descriptions.Item>
+        <Descriptions.Item label="请求时间">
+          {moment(data?.requestTime).format('YYYY-MM-DD HH:mm:ss')}
+        </Descriptions.Item>
+        <Descriptions.Item label="请求耗时" span={2}>
+          {(data?.responseTime || 0) - (data?.requestTime || 0)}ms
+        </Descriptions.Item>
+        <Descriptions.Item label="请求头" span={3}>
+          {JSON.stringify(data?.httpHeaders)}
+        </Descriptions.Item>
+        <Descriptions.Item label="请求参数" span={3}>
+          {JSON.stringify(data?.parameters)}
+        </Descriptions.Item>
+        <Descriptions.Item label="异常信息" span={3}>
+          {data?.exception}
+        </Descriptions.Item>
+      </Descriptions>
+    </Modal>
+  );
+};
+export default Detail;

+ 136 - 0
src/pages/Log/Access/index.tsx

@@ -0,0 +1,136 @@
+import BaseService from '@/utils/BaseService';
+import { useRef, useState } from 'react';
+import type { ProColumns, ActionType } from '@jetlinks/pro-table';
+import type { AccessLogItem } from '@/pages/Log/Access/typings';
+import moment from 'moment';
+import { Tag, Tooltip } from 'antd';
+import { EyeOutlined } from '@ant-design/icons';
+import { useIntl } from '@@/plugin-locale/localeExports';
+import ProTable from '@jetlinks/pro-table';
+import SearchComponent from '@/components/SearchComponent';
+import Detail from '@/pages/Log/Access/Detail';
+
+const service = new BaseService('logger/access');
+
+const Access = () => {
+  const actionRef = useRef<ActionType>();
+  const intl = useIntl();
+  const [param, setParam] = useState({});
+  const [visible, setVisible] = useState<boolean>(false);
+  const [current, setCurrent] = useState<Partial<AccessLogItem>>({});
+
+  const columns: ProColumns<AccessLogItem>[] = [
+    {
+      title: 'IP',
+      dataIndex: 'ip',
+      ellipsis: true,
+    },
+    {
+      title: intl.formatMessage({
+        id: 'pages.log.access.url',
+        defaultMessage: '请求路径',
+      }),
+      dataIndex: 'url',
+      ellipsis: true,
+    },
+    {
+      title: '请求方法',
+      dataIndex: 'httpMethod',
+      ellipsis: true,
+    },
+    {
+      title: intl.formatMessage({
+        id: 'pages.table.description',
+        defaultMessage: '说明',
+      }),
+      dataIndex: 'description',
+      ellipsis: true,
+      render: (text, record) => {
+        return `${record.action}-${record.describe}`;
+      },
+    },
+    {
+      title: intl.formatMessage({
+        id: 'pages.log.access.requestTime',
+        defaultMessage: '请求时间',
+      }),
+      dataIndex: 'requestTime',
+      sorter: true,
+      valueType: 'dateTime',
+      defaultSortOrder: 'descend',
+      ellipsis: true,
+      width: 200,
+      renderText: (text: string) => moment(text).format('YYYY-MM-DD HH:mm:ss'),
+    },
+    {
+      title: intl.formatMessage({
+        id: 'pages.log.access.requestTimeConsuming',
+        defaultMessage: '请求耗时',
+      }),
+      renderText: (record: AccessLogItem) => (
+        <Tag color="purple">{record.responseTime - record.requestTime}ms</Tag>
+      ),
+    },
+    {
+      title: intl.formatMessage({
+        id: 'pages.log.access.requestUser',
+        defaultMessage: '请求用户',
+      }),
+      dataIndex: 'context.username',
+      render: (text) => <Tag color="geekblue">{text}</Tag>,
+    },
+    {
+      title: intl.formatMessage({
+        id: 'pages.data.option',
+        defaultMessage: '操作',
+      }),
+      valueType: 'option',
+      align: 'center',
+      render: (text, record) => [
+        <a
+          key="editable"
+          onClick={() => {
+            setVisible(true);
+            setCurrent(record);
+          }}
+        >
+          <Tooltip title={'查看'}>
+            <EyeOutlined />
+          </Tooltip>
+        </a>,
+      ],
+    },
+  ];
+  return (
+    <>
+      <SearchComponent<AccessLogItem>
+        field={columns}
+        target="access-log"
+        onSearch={(data) => {
+          actionRef.current?.reset?.();
+          setParam(data);
+        }}
+      />
+      <ProTable<AccessLogItem>
+        columns={columns}
+        params={param}
+        request={async (params) =>
+          service.query({ ...params, sorts: [{ name: 'responseTime', order: 'desc' }] })
+        }
+        defaultParams={{ sorts: [{ responseTime: 'desc' }] }}
+        search={false}
+        actionRef={actionRef}
+      />
+      {visible && (
+        <Detail
+          data={current}
+          close={() => {
+            setVisible(false);
+            setCurrent({});
+          }}
+        />
+      )}
+    </>
+  );
+};
+export default Access;

+ 16 - 0
src/pages/Log/Access/typings.d.ts

@@ -0,0 +1,16 @@
+export type AccessLogItem = {
+  id: string;
+  context: any;
+  describe: string;
+  exception: string;
+  httpHeaders: any;
+  httpMethod: string;
+  ip: string;
+  method: string;
+  parameters: any;
+  requestTime: number;
+  responseTime: number;
+  target: string;
+  url: string;
+  action: string;
+};

+ 35 - 0
src/pages/Log/System/Detail/index.tsx

@@ -0,0 +1,35 @@
+import { Input, Modal, Space, Tag } from 'antd';
+import type { SystemLogItem } from '@/pages/Log/System/typings';
+import { useEffect, useState } from 'react';
+import moment from 'moment';
+
+interface Props {
+  data: Partial<SystemLogItem>;
+  close: () => void;
+}
+
+const Detail = (props: Props) => {
+  const [data, setDada] = useState<Partial<SystemLogItem>>(props.data || {});
+
+  useEffect(() => {
+    setDada(props.data);
+  }, [props.data]);
+
+  return (
+    <Modal title={'详情'} visible onCancel={props.close} onOk={props.close} width={1000}>
+      <Space>
+        <span>[{data?.threadName}]</span>
+        <span>{moment(data?.createTime).format('YYYY-MM-DD HH:mm:ss')}</span>
+        <span>{data?.className}</span>
+      </Space>
+      <p>
+        <Tag color={data?.level === 'ERROR' ? 'red' : 'orange'}>{data?.level}</Tag>
+        {data?.message}
+      </p>
+      <div>
+        <Input.TextArea rows={20} value={data?.exceptionStack} />
+      </div>
+    </Modal>
+  );
+};
+export default Detail;

+ 140 - 0
src/pages/Log/System/index.tsx

@@ -0,0 +1,140 @@
+import { useIntl } from '@@/plugin-locale/localeExports';
+import { useRef, useState } from 'react';
+import type { ProColumns, ActionType } from '@jetlinks/pro-table';
+import type { SystemLogItem } from '@/pages/Log/System/typings';
+import { Tag, Tooltip } from 'antd';
+import moment from 'moment';
+import BaseService from '@/utils/BaseService';
+import { EyeOutlined } from '@ant-design/icons';
+import ProTable from '@jetlinks/pro-table';
+import SearchComponent from '@/components/SearchComponent';
+import Detail from '@/pages/Log/System/Detail';
+
+const service = new BaseService<SystemLogItem>('logger/system');
+const System = () => {
+  const intl = useIntl();
+  const actionRef = useRef<ActionType>();
+  const [param, setParam] = useState({});
+  const [visible, setVisible] = useState<boolean>(false);
+  const [current, setCurrent] = useState<Partial<SystemLogItem>>({});
+
+  const columns: ProColumns<SystemLogItem>[] = [
+    {
+      title: intl.formatMessage({
+        id: 'pages.table.name',
+        defaultMessage: '名称',
+      }),
+      dataIndex: 'name',
+      ellipsis: true,
+    },
+    {
+      title: '日志级别',
+      dataIndex: 'level',
+      width: 80,
+      render: (text) => <Tag color={text === 'ERROR' ? 'red' : 'orange'}>{text}</Tag>,
+      valueType: 'select',
+      valueEnum: {
+        ERROR: {
+          text: 'ERROR',
+          status: 'ERROR',
+        },
+        INFO: {
+          text: 'INFO',
+          status: 'INFO',
+        },
+        DEBUG: {
+          text: 'DEBUG',
+          status: 'DEBUG',
+        },
+        WARN: {
+          text: 'WARN',
+          status: 'WARN',
+        },
+      },
+    },
+    {
+      title: intl.formatMessage({
+        id: 'pages.log.system.logContent',
+        defaultMessage: '日志内容',
+      }),
+      dataIndex: 'exceptionStack',
+      ellipsis: true,
+    },
+    {
+      title: intl.formatMessage({
+        id: 'pages.log.system.serviceName',
+        defaultMessage: '服务名',
+      }),
+      dataIndex: 'context.server',
+      width: 150,
+      ellipsis: true,
+    },
+    {
+      title: intl.formatMessage({
+        id: 'pages.log.system.creationTime',
+        defaultMessage: '创建时间',
+      }),
+      dataIndex: 'createTime',
+      width: 200,
+      sorter: true,
+      ellipsis: true,
+      valueType: 'dateTime',
+      defaultSortOrder: 'descend',
+      renderText: (text) => moment(text).format('YYYY-MM-DD HH:mm:ss'),
+    },
+    {
+      title: intl.formatMessage({
+        id: 'pages.data.option',
+        defaultMessage: '操作',
+      }),
+      valueType: 'option',
+      align: 'center',
+      width: 200,
+      render: (text, record) => [
+        <a
+          key="editable"
+          onClick={() => {
+            setVisible(true);
+            setCurrent(record);
+          }}
+        >
+          <Tooltip title="查看">
+            <EyeOutlined />
+          </Tooltip>
+        </a>,
+      ],
+    },
+  ];
+  return (
+    <>
+      <SearchComponent<SystemLogItem>
+        field={columns}
+        target="system-log"
+        onSearch={(data) => {
+          actionRef.current?.reset?.();
+          setParam(data);
+        }}
+      />
+      <ProTable<SystemLogItem>
+        columns={columns}
+        params={param}
+        request={async (params) =>
+          service.query({ ...params, sorts: [{ name: 'createTime', order: 'desc' }] })
+        }
+        defaultParams={{ sorts: [{ createTime: 'desc' }] }}
+        search={false}
+        actionRef={actionRef}
+      />
+      {visible && (
+        <Detail
+          data={current}
+          close={() => {
+            setVisible(false);
+            setCurrent({});
+          }}
+        />
+      )}
+    </>
+  );
+};
+export default System;

+ 14 - 0
src/pages/Log/System/typings.d.ts

@@ -0,0 +1,14 @@
+export type SystemLogItem = {
+  id: string;
+  className: string;
+  context: any;
+  createTime: number;
+  exceptionStack: string;
+  level: string;
+  lineNumber: number;
+  message: string;
+  methodName: string;
+  name: string;
+  threadId: string;
+  threadName: string;
+};

+ 28 - 0
src/pages/Log/index.tsx

@@ -0,0 +1,28 @@
+import { PageContainer } from '@ant-design/pro-layout';
+import { useState } from 'react';
+import Access from '@/pages/Log/Access';
+import System from '@/pages/Log/System';
+
+const Log = () => {
+  const [tab, setTab] = useState<string>('access');
+  const list = [
+    {
+      key: 'access',
+      tab: '访问日志',
+      component: <Access />,
+    },
+    {
+      key: 'system',
+      tab: '系统日志',
+      component: <System />,
+    },
+  ];
+
+  return (
+    <PageContainer onTabChange={setTab} tabList={list} tabActiveKey={tab}>
+      {list.find((k) => k.key === tab)?.component}
+    </PageContainer>
+  );
+};
+
+export default Log;

+ 0 - 163
src/pages/device/Instance/Detail/Running/Property/PropertyCard copy.tsx

@@ -1,163 +0,0 @@
-import { EditOutlined, SyncOutlined, UnorderedListOutlined } from '@ant-design/icons';
-import { Button, Divider, Input, message, Popover, Spin, Tooltip } from 'antd';
-import ProCard from '@ant-design/pro-card';
-import type { ObserverMetadata, PropertyMetadata } from '@/pages/device/Product/typings';
-import { Line } from '@ant-design/charts';
-import { useCallback, useEffect, useRef, useState } from 'react';
-import { service } from '@/pages/device/Instance';
-import { useParams } from 'umi';
-import PropertyLog from '@/pages/device/Instance/Detail/MetadataLog/Property';
-
-interface Props {
-  data: Partial<PropertyMetadata> & ObserverMetadata;
-}
-
-type Payload = {
-  timeString: string;
-  timestamp: number;
-  value: number;
-  formatValue: string;
-  property: string;
-} & Record<string, unknown>;
-const Property = (props: Props) => {
-  const { data } = props;
-  const [list, setList] = useState<Record<string, unknown>[]>([]);
-  const cacheList = useRef<Record<string, unknown>[]>(list);
-  const [title, setTitle] = useState<string>('');
-
-  const params = useParams<{ id: string }>();
-  const type = data.valueType?.type;
-  const value = list[list.length - 1];
-
-  useEffect(() => {
-    data.subscribe((payload: any) => {
-      if (payload instanceof Array) {
-        setTitle(`:${payload[payload?.length - 1].formatValue}`);
-        cacheList.current = payload;
-        setList(payload);
-      } else if (payload instanceof Object) {
-        const temp: Payload = {
-          timeString: payload.timeString,
-          timestamp: payload.timestamp,
-          ...payload.value,
-        };
-        // title
-        const newValue = temp?.formatValue;
-        setTitle(`:${newValue}`);
-        // list
-        const cache = cacheList.current;
-        cache.shift();
-        cache.push(temp);
-        setList(cache);
-      }
-    });
-  }, [data]);
-
-  const chart = useCallback(() => {
-    switch (type) {
-      case 'int':
-      case 'float':
-      case 'double':
-      case 'long':
-      case 'enum':
-        return (
-          <Line
-            height={60}
-            xField="timeString"
-            yField="value"
-            xAxis={false}
-            yAxis={false}
-            data={list}
-          />
-        );
-      case 'object':
-        return <div>{JSON.stringify(value.formatValue) || '/'}</div>;
-      default:
-        return null;
-    }
-  }, [list, type]);
-
-  const [loading, setLoading] = useState<boolean>(false);
-  const refreshProperty = async () => {
-    setLoading(true);
-    if (!data.id) return;
-    const resp = await service.getProperty(params.id, data.id);
-    setLoading(false);
-    if (resp.status === 200) {
-      message.success('操作成功');
-    }
-  };
-
-  const [propertyValue, setPropertyValue] = useState<string>();
-  const [visible, setVisible] = useState<boolean>(false);
-  const handleSetPropertyValue = async () => {
-    const resp = await service.setProperty(params.id, { [`${data.id}`]: propertyValue });
-    if (resp.status === 200) {
-      message.success('操作成功');
-    }
-  };
-
-  const renderSetProperty = () => {
-    if (data.expands?.readOnly === false || data.expands?.readOnly === 'false') {
-      return (
-        <Popover
-          trigger="click"
-          title={
-            <div
-              style={{
-                display: 'flex',
-                justifyContent: 'space-between',
-              }}
-            >
-              <span>设置属性</span>
-              <Button size="small" type="primary" onClick={handleSetPropertyValue}>
-                设置
-              </Button>
-            </div>
-          }
-          content={
-            <Input value={propertyValue} onChange={(e) => setPropertyValue(e.target.value)} />
-          }
-        >
-          <Tooltip placement="top" title="设置属性至设备">
-            <EditOutlined />
-          </Tooltip>
-          <Divider type="vertical" />
-        </Popover>
-      );
-    } else {
-      return null;
-    }
-  };
-  return (
-    <ProCard
-      title={`${data?.name} ${title}`}
-      extra={
-        <>
-          {renderSetProperty()}
-          <Tooltip placement="top" title="获取最新属性值">
-            <SyncOutlined onClick={refreshProperty} />
-          </Tooltip>
-          <Divider type="vertical" />
-          <Tooltip placement="top" title="详情">
-            <UnorderedListOutlined
-              onClick={() => {
-                setVisible(true);
-              }}
-            />
-          </Tooltip>
-        </>
-      }
-      layout="center"
-      bordered
-      headerBordered
-      colSpan={{ xs: 12, sm: 8, md: 6, lg: 6, xl: 6 }}
-    >
-      <Spin spinning={loading}>
-        <div style={{ height: 60 }}>{chart()}</div>
-      </Spin>
-      <PropertyLog data={data} visible={visible} close={() => setVisible(false)} />
-    </ProCard>
-  );
-};
-export default Property;

+ 12 - 0
src/pages/device/Instance/Detail/Running/Property/PropertyCard.less

@@ -0,0 +1,12 @@
+:global {
+  .ant-pro-card-body {
+    padding-top: 0;
+  }
+}
+
+.value {
+  width: '100%';
+  overflow: hidden;
+  white-space: nowrap;
+  text-overflow: ellipsis;
+}

+ 18 - 3
src/pages/device/Instance/Detail/Running/Property/PropertyCard.tsx

@@ -8,6 +8,7 @@ import { useParams } from 'umi';
 import PropertyLog from '@/pages/device/Instance/Detail/MetadataLog/Property';
 import EditProperty from '@/pages/device/Instance/Detail/Running/Property/EditProperty';
 import moment from 'moment';
+import './PropertyCard.less';
 
 interface Props {
   data: Partial<PropertyMetadata>;
@@ -33,9 +34,17 @@ const Property = (props: Props) => {
   const [visible, setVisible] = useState<boolean>(false);
   const [editVisible, setEditVisible] = useState<boolean>(false);
 
+  const renderTitle = (title: string) => {
+    return (
+      <div className="value" style={{ color: 'rgba(0, 0, 0, .65)', fontSize: 14 }}>
+        {title}
+      </div>
+    );
+  };
+
   return (
     <ProCard
-      title={`${data?.name}`}
+      title={renderTitle(data?.name || '')}
       extra={
         <>
           {(data.expands?.readOnly === false || data.expands?.readOnly === 'false') && (
@@ -66,12 +75,18 @@ const Property = (props: Props) => {
       bordered
       hoverable
       colSpan={{ xs: 12, sm: 8, md: 6, lg: 6, xl: 6 }}
+      style={{ backgroundColor: 'rgba(0, 0, 0, .02)' }}
     >
       <Spin spinning={loading}>
         <div>
-          <div style={{ fontWeight: 600, fontSize: '30px' }}>{value?.formatValue || '--'}</div>
+          <div className="value" style={{ fontWeight: 700, fontSize: '24px', color: '#323130' }}>
+            {value?.formatValue || '--'}
+          </div>
           <div style={{ marginTop: 10 }}>
-            {value?.timestamp ? moment(value?.timestamp).format('YYYY-MM-DD HH:mm:ss') : '--'}
+            <div style={{ color: 'rgba(0, 0, 0, .65)', fontSize: 12 }}>更新时间</div>
+            <div style={{ marginTop: 5, fontSize: 16, color: 'black' }} className="value">
+              {value?.timestamp ? moment(value?.timestamp).format('YYYY-MM-DD HH:mm:ss') : '--'}
+            </div>
           </div>
         </div>
       </Spin>

+ 1 - 1
src/pages/device/Instance/Detail/Running/Property/index.tsx

@@ -191,7 +191,7 @@ const Property = (props: Props) => {
                 });
               }
             }}
-            style={{ width: 300 }}
+            style={{ width: 317 }}
           />
         </Space>
         <CheckButton

+ 32 - 3
src/pages/device/Instance/Detail/Running/index.tsx

@@ -1,19 +1,48 @@
 import { InstanceModel } from '@/pages/device/Instance';
-import { Card, Tabs } from 'antd';
+import { Card, Input, Tabs } from 'antd';
 import type { DeviceMetadata } from '@/pages/device/Product/typings';
 import Property from '@/pages/device/Instance/Detail/Running/Property';
 import Event from '@/pages/device/Instance/Detail/Running/Event';
+import { useEffect, useState } from 'react';
 
 const Running = () => {
   const metadata = JSON.parse((InstanceModel.detail?.metadata || '{}') as string) as DeviceMetadata;
+  const [list, setList] = useState<any[]>([]);
+
+  useEffect(() => {
+    setList(metadata?.events || []);
+  }, [InstanceModel.detail?.metadata]);
+
+  const operations = () => (
+    <Input.Search
+      style={{ maxWidth: 200, marginBottom: 10 }}
+      allowClear
+      placeholder="请输入名称"
+      onSearch={(value: string) => {
+        if (value) {
+          const li = list.filter((i) => {
+            return i?.name.indexOf(value) !== -1;
+          });
+          setList(li);
+        } else {
+          setList(metadata?.events || []);
+        }
+      }}
+    />
+  );
 
   return (
     <Card>
-      <Tabs defaultActiveKey="1" tabPosition="left" style={{ height: 600 }}>
+      <Tabs
+        defaultActiveKey="1"
+        tabPosition="left"
+        style={{ height: 600 }}
+        tabBarExtraContent={{ left: operations() }}
+      >
         <Tabs.TabPane tab="属性" key="1">
           <Property data={metadata?.properties || []} />
         </Tabs.TabPane>
-        {metadata.events?.map((item) => (
+        {list?.map((item) => (
           <Tabs.TabPane tab={item.name} key={item.id}>
             <Event data={item} />
           </Tabs.TabPane>

+ 128 - 0
src/pages/device/Product/Detail/Access/AccessConfig/index.less

@@ -0,0 +1,128 @@
+// .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;
+// }
+
+// .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;
+// }
+
+.context {
+  display: flex;
+  width: 100%;
+  .card {
+    display: flex;
+    flex-direction: column;
+    width: 100%;
+    margin-left: 10px;
+    .header {
+      .title {
+        width: 90%;
+        overflow: hidden;
+        font-weight: 700;
+        font-size: 18px;
+        white-space: nowrap;
+        text-overflow: ellipsis;
+      }
+      .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;
+  }
+}
+
+.search {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  margin-bottom: 24px;
+
+  :global {
+    .ant-card {
+      margin-bottom: 0 !important;
+      border: none !important;
+    }
+
+    .ant-formily-item-feedback-layout-loose {
+      margin-bottom: 0 !important;
+    }
+
+    .ant-card-body {
+      padding: 24px !important;
+    }
+  }
+}

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

@@ -1,12 +1,14 @@
 import { useEffect, useState } from 'react';
-import { Badge, Button, Card, Col, message, Modal, Pagination, Row } from 'antd';
+import { Badge, Button, Col, message, Modal, Pagination, Row } from 'antd';
 import { service } from '@/pages/link/AccessConfig';
 import { productModel } from '@/pages/device/Product';
 import SearchComponent from '@/components/SearchComponent';
 import type { ProColumns } from '@jetlinks/pro-table';
-import styles from '../index.less';
+import styles from './index.less';
 import Service from '@/pages/device/Product/service';
-
+import { TableCard } from '@/components';
+import { StatusColorEnum } from '@/components/BadgeStatus';
+const defaultImage = require('/public/images/device-access.png');
 interface Props {
   close: () => void;
   data?: any;
@@ -24,7 +26,7 @@ const AccessConfig = (props: Props) => {
   });
   const [param, setParam] = useState<any>({ pageSize: 4 });
 
-  const [currrent, setCurrrent] = useState<any>({
+  const [currrent] = useState<any>({
     id: productModel.current?.accessId,
     name: productModel.current?.accessName,
     protocol: productModel.current?.messageProtocol,
@@ -111,7 +113,6 @@ const AccessConfig = (props: Props) => {
       <div className={styles.search}>
         <SearchComponent
           field={columns}
-          // pattern={'simple'}
           enableSave={false}
           onSearch={(data: any) => {
             const dt = {
@@ -120,9 +121,6 @@ const AccessConfig = (props: Props) => {
             };
             handleSearch(dt);
           }}
-          // onReset={() => {
-          //   handleSearch({ pageSize: 4 });
-          // }}
         />
         <div style={{ display: 'flex', justifyContent: 'flex-end' }}>
           <Button
@@ -142,36 +140,38 @@ const AccessConfig = (props: Props) => {
       </div>
       <Row gutter={[16, 16]}>
         {dataSource.data.map((item: any) => (
-          <Col key={item.name} span={12}>
-            <Card
-              style={{
-                width: '100%',
-                border: currrent?.id === item.id ? '1px solid #1d39c4' : '',
-              }}
-              hoverable
-              onClick={() => {
-                setCurrrent(item);
+          <Col
+            key={item.name}
+            span={12}
+            // style={{
+            //   width: '100%',
+            //   borderColor: currrent?.id === item.id ? 'var(--ant-primary-color-active)' : ''
+            // }}
+            // onClick={() => {
+            //   setCurrrent(item);
+            // }}
+          >
+            <TableCard
+              showMask={false}
+              status={item.state.value}
+              statusText={item.state.text}
+              statusNames={{
+                enabled: StatusColorEnum.processing,
+                disabled: StatusColorEnum.error,
               }}
             >
-              <div className={styles.box}>
-                <div className={styles.images}>{item.name}</div>
-                <div className={styles.content}>
+              <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.top}>
-                      <div className={styles.title}>{item.name}</div>
-                      <div className={styles.status}>
-                        <Badge
-                          color={item.state.value === 'disabled' ? 'red' : 'green'}
-                          text={item.state.text}
-                          style={{ marginLeft: '20px' }}
-                        />
-                      </div>
-                    </div>
-                    <div className={styles.desc}>这里是接入方式的解释说明</div>
+                    <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.title}>{item?.channelInfo?.name}</div>
+                      <div className={styles.subTitle}>{item?.channelInfo?.name || '--'}</div>
                       <p>
                         {item.channelInfo?.addresses.map((i: any) => (
                           <div key={i.address}>
@@ -181,13 +181,13 @@ const AccessConfig = (props: Props) => {
                       </p>
                     </div>
                     <div className={styles.procotol}>
-                      <div className={styles.title}>{item?.protocolDetail?.name}</div>
-                      <p style={{ color: 'rgba(0, 0, 0, .55)' }}>{item.description}</p>
+                      <div className={styles.subTitle}>{item?.protocolDetail?.name || '--'}</div>
+                      <p>{item.protocolDetail?.description || '--'}</p>
                     </div>
                   </div>
                 </div>
               </div>
-            </Card>
+            </TableCard>
           </Col>
         ))}
       </Row>

+ 14 - 98
src/pages/device/Product/Detail/Access/index.less

@@ -1,113 +1,29 @@
-.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;
-}
-
-.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;
-}
-
-.title {
-  font-weight: 600;
-  font-size: 14px;
-}
-
-.container {
-  display: flex;
-  align-items: center;
-  width: 100%;
-  height: 90px;
-  margin-top: 10px;
-}
-
-.server {
-  width: 50%;
-  margin-right: 20px;
-}
-
-.procotol {
-  display: -webkit-box;
-  width: 50%;
-  overflow: hidden;
-  text-overflow: ellipsis;
-  -webkit-box-orient: vertical;
-  -webkit-line-clamp: 4;
-}
-
-:global {
-  .ant-pagination-item {
-    display: none;
-  }
-}
-
 .config {
-  width: 100%;
-
   .title {
     width: 100%;
     margin-bottom: 10px;
+    color: rgba(0, 0, 0, 0.8);
     font-weight: 600;
   }
-
   .title::before {
     margin-right: 10px;
     background-color: #2810ff;
     content: '|';
   }
-}
-
-.search {
-  display: flex;
-  align-items: center;
-  justify-content: space-between;
-  margin-bottom: 24px;
-
-  :global {
-    .ant-card {
-      margin-bottom: 0 !important;
-      border: none !important;
-    }
-
-    .ant-formily-item-feedback-layout-loose {
-      margin-bottom: 0 !important;
-    }
 
-    .ant-card-body {
-      padding: 24px !important;
+  .item {
+    margin-bottom: 10px;
+    .context {
+      margin: 5px 0;
+      color: rgba(0, 0, 0, 0.8);
     }
   }
 }
+.info {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  height: 630px;
+  padding: 20px;
+  background-color: #e6e6e6;
+}

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

@@ -1,10 +1,22 @@
-import { Badge, Empty, Table, Tooltip } from 'antd';
+import { Badge, Button, Col, Empty, message, Row, Table, Tooltip } from 'antd';
 import { service } from '@/pages/link/AccessConfig';
+import { productModel, service as productService } from '@/pages/device/Product';
 import styles from './index.less';
 import { useEffect, useState } from 'react';
-import { productModel } from '@/pages/device/Product';
 import AccessConfig from './AccessConfig';
 import ReactMarkdown from 'react-markdown';
+import { Form, FormGrid, FormItem, FormLayout, Input, Password, PreviewText } from '@formily/antd';
+import type { ISchema } from '@formily/json-schema';
+import type { ConfigProperty } from '@/pages/device/Product/typings';
+import { createSchemaField } from '@formily/react';
+import { createForm } from '@formily/core';
+import type { SetStateAction } from 'react';
+import { QuestionCircleOutlined } from '@ant-design/icons';
+
+const componentMap = {
+  string: 'Input',
+  password: 'Password',
+};
 
 const Access = () => {
   const [visible, setVisible] = useState<boolean>(true);
@@ -24,6 +36,8 @@ const Access = () => {
 
   const [configVisible, setConfigVisible] = useState<boolean>(false);
 
+  const [metadata, setMetadata] = useState<ConfigMetadata[]>([]);
+
   const queryNetworkList = (id: string) => {
     service.getNetworkList(MetworkTypeMapping.get(id)).then((resp) => {
       if (resp.status === 200) {
@@ -188,7 +202,16 @@ const Access = () => {
     });
   };
 
+  const id = productModel.current?.id;
+
   useEffect(() => {
+    if (id) {
+      productService
+        .getConfigMetadata(id)
+        .then((resp: { result: SetStateAction<ConfigMetadata[]> }) => {
+          setMetadata(resp.result);
+        });
+    }
     queryProviders();
     setVisible(!!productModel.current?.accessId);
     if (productModel.current?.accessId) {
@@ -200,6 +223,101 @@ const Access = () => {
     }
   }, [productModel.current]);
 
+  const form = createForm({
+    validateFirst: true,
+    initialValues: productModel.current?.configuration,
+  });
+
+  const SchemaField = createSchemaField({
+    components: {
+      Password,
+      FormGrid,
+      PreviewText,
+      FormItem,
+      Input,
+    },
+  });
+
+  const configToSchema = (data: ConfigProperty[]) => {
+    const obj = {};
+    data.forEach((item) => {
+      obj[item?.property] = {
+        type: 'string',
+        title: item.name,
+        'x-decorator': 'FormItem',
+        'x-component': componentMap[item.type.type],
+        'x-decorator-props': {
+          tooltip: item.description,
+          gridSpan: 1,
+          labelAlign: 'left',
+          layout: 'vertical',
+        },
+      };
+    });
+    return obj;
+  };
+
+  const getDetailInfo = () => {
+    productService.getProductDetail(id || '').subscribe((data) => {
+      if (data) {
+        productModel.current = {
+          ...productModel.current,
+          ...data,
+        };
+      }
+    });
+  };
+
+  const renderConfigCard = () => {
+    return (
+      metadata &&
+      metadata.length > 0 &&
+      metadata?.map((item: any) => {
+        const itemSchema: ISchema = {
+          type: 'object',
+          properties: {
+            grid: {
+              type: 'void',
+              'x-component': 'FormGrid',
+              'x-component-props': {
+                maxColumns: 1,
+                minColumns: 1,
+                columnGap: 48,
+              },
+              properties: configToSchema(item.properties),
+            },
+          },
+        };
+
+        return (
+          <PreviewText.Placeholder value="-" key={'config'}>
+            <Form form={form} layout="vertical">
+              <FormLayout>
+                <SchemaField schema={itemSchema} />
+              </FormLayout>
+              <Button
+                type="primary"
+                onClick={async () => {
+                  const values = (await form.submit()) as any;
+                  const resp = await productService.modify(id || '', {
+                    id,
+                    configuration: { ...values },
+                  });
+                  if (resp.status === 200) {
+                    message.success('操作成功!');
+                    getDetailInfo();
+                  }
+                }}
+              >
+                保存
+              </Button>
+            </Form>
+          </PreviewText.Placeholder>
+        );
+      })
+    );
+  };
+
   return (
     <div>
       {!visible ? (
@@ -221,51 +339,96 @@ const Access = () => {
           />
         </div>
       ) : (
-        <div className={styles.config}>
-          <div>
-            <div className={styles.title}>
-              接入方式
-              <a
-                style={{ marginLeft: 20 }}
-                onClick={() => {
-                  setConfigVisible(true);
-                }}
-              >
-                更换
-              </a>
-            </div>
-            {providers.find((i) => i.id === access?.provider)?.name || ''}
-          </div>
-          {providers.find((i) => i.id === access?.provider)?.description && (
-            <span>{providers.find((i) => i.id === access?.provider)?.description || ''}</span>
-          )}
-          <div className={styles.title}>消息协议</div>
-          {access?.protocolDetail?.name || ''}
-          <ReactMarkdown>{config?.document}</ReactMarkdown>
-          <div className={styles.title}>网络组件</div>
-          {(networkList.find((i) => i.id === access?.channelId)?.addresses || []).map(
-            (item: any) => (
-              <div key={item.address}>
-                <Badge
-                  color={item.health === -1 ? 'red' : 'green'}
-                  text={item.address}
-                  style={{ marginLeft: '20px' }}
-                />
+        <Row gutter={24}>
+          <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>
+                <div className={styles.context}>
+                  {providers.find((i) => i.id === access?.provider)?.name || '--'}
+                </div>
+                <div className={styles.context}>
+                  {providers.find((i) => i.id === access?.provider)?.description && (
+                    <span>
+                      {providers.find((i) => i.id === access?.provider)?.description || '--'}
+                    </span>
+                  )}
+                </div>
+              </div>
+
+              <div className={styles.item}>
+                <div className={styles.title}>消息协议</div>
+                <div className={styles.context}>{access?.protocolDetail?.name || '--'}</div>
+                <div className={styles.context}>
+                  <ReactMarkdown>{config?.document || '--'}</ReactMarkdown>
+                </div>
+              </div>
+
+              <div className={styles.item}>
+                <div className={styles.title}>连接信息</div>
+                {(networkList.find((i) => i.id === access?.channelId)?.addresses || []).length > 0
+                  ? (networkList.find((i) => i.id === access?.channelId)?.addresses || []).map(
+                      (item: any) => (
+                        <div key={item.address}>
+                          <Badge
+                            color={item.health === -1 ? 'red' : 'green'}
+                            text={item.address}
+                            style={{ marginLeft: '20px' }}
+                          />
+                        </div>
+                      ),
+                    )
+                  : '---'}
+              </div>
+
+              <div className={styles.item}>
+                <div className={styles.title}>
+                  认证配置
+                  <Tooltip title="此配置来自于该产品接入方式所选择的协议">
+                    <QuestionCircleOutlined />
+                  </Tooltip>
+                </div>
+                {renderConfigCard()}
               </div>
-            ),
-          )}
-          {config?.routes && config?.routes?.length > 0 && (
-            <div>
-              <Table
-                dataSource={config?.routes || []}
-                bordered
-                columns={config.id === 'MQTT' ? columnsMQTT : columnsHTTP}
-                pagination={false}
-                scroll={{ y: 240 }}
-              />
             </div>
-          )}
-        </div>
+          </Col>
+          <Col span={12}>
+            <div className={styles.info}>
+              {config?.routes && config?.routes?.length > 0 ? (
+                <div>
+                  <div style={{ fontWeight: '600', marginBottom: 10 }}>
+                    {access?.provider === 'mqtt-server-gateway' ||
+                    access?.provider === 'mqtt-client-gateway'
+                      ? 'topic'
+                      : 'URL信息'}
+                  </div>
+                  <Table
+                    dataSource={config?.routes || []}
+                    bordered
+                    columns={config.id === 'MQTT' ? columnsMQTT : columnsHTTP}
+                    pagination={false}
+                    scroll={{ y: 500 }}
+                  />
+                </div>
+              ) : (
+                <Empty />
+              )}
+            </div>
+          </Col>
+        </Row>
       )}
       {configVisible && (
         <AccessConfig

+ 118 - 124
src/pages/device/Product/Detail/BaseInfo/index.tsx

@@ -1,69 +1,63 @@
 import { productModel, service } from '@/pages/device/Product';
-import { Button, Card, Descriptions, Divider, message } from 'antd';
-import type { SetStateAction } from 'react';
-import { useEffect, useState } from 'react';
+import { Button, Descriptions } from 'antd';
+import { useState } from 'react';
 import { useIntl } from '@@/plugin-locale/localeExports';
 import { EditOutlined } from '@ant-design/icons';
 import { getDateFormat } from '@/utils/util';
 import Save from '@/pages/device/Product/Save';
-import { Form, FormGrid, FormItem, FormLayout, Input, Password, PreviewText } from '@formily/antd';
-import type { ISchema } from '@formily/json-schema';
-import type { ConfigMetadata, ConfigProperty } from '@/pages/device/Product/typings';
-import { createSchemaField } from '@formily/react';
-import { createForm } from '@formily/core';
 
-const componentMap = {
-  string: 'Input',
-  password: 'Password',
-};
+// const componentMap = {
+//   string: 'Input',
+//   password: 'Password',
+// };
 
 const BaseInfo = () => {
   const intl = useIntl();
-  const [metadata, setMetadata] = useState<ConfigMetadata[]>([]);
-  const [state, setState] = useState<boolean>(false);
+  // const [metadata, setMetadata] = useState<ConfigMetadata[]>([]);
+  // const [state, setState] = useState<boolean>(false);
   const [visible, setVisible] = useState(false);
 
-  const form = createForm({
-    validateFirst: true,
-    readPretty: state,
-    initialValues: productModel.current?.configuration,
-  });
+  // const form = createForm({
+  //   validateFirst: true,
+  //   readPretty: state,
+  //   initialValues: productModel.current?.configuration,
+  // });
 
   const id = productModel.current?.id;
 
-  useEffect(() => {
-    if (id) {
-      service.getConfigMetadata(id).then((config: { result: SetStateAction<ConfigMetadata[]> }) => {
-        setMetadata(config.result);
-      });
-    }
-  }, [productModel.current]);
+  // useEffect(() => {
+  //   if (id) {
+  //     service.getConfigMetadata(id).then((config: { result: SetStateAction<ConfigMetadata[]> }) => {
+  //       setMetadata(config.result);
+  //     });
+  //   }
+  // }, [productModel.current]);
 
-  const SchemaField = createSchemaField({
-    components: {
-      Password,
-      FormGrid,
-      PreviewText,
-      FormItem,
-      Input,
-    },
-  });
+  // const SchemaField = createSchemaField({
+  //   components: {
+  //     Password,
+  //     FormGrid,
+  //     PreviewText,
+  //     FormItem,
+  //     Input,
+  //   },
+  // });
 
-  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 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 getDetailInfo = () => {
     service.getProductDetail(id || '').subscribe((data) => {
@@ -76,80 +70,80 @@ const BaseInfo = () => {
     });
   };
 
-  const renderConfigCard = () => {
-    return (
-      metadata &&
-      metadata.length > 0 &&
-      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 renderConfigCard = () => {
+  //   return (
+  //     metadata &&
+  //     metadata.length > 0 &&
+  //     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),
+  //           },
+  //         },
+  //       };
 
-        return (
-          <>
-            <Divider />
-            <Card
-              key={item.name}
-              title={item.name}
-              extra={
-                <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) {
-                        message.success('操作成功!');
-                        getDetailInfo();
-                      }
-                    }
-                    setState(!state);
-                  }}
-                >
-                  {state ? (
-                    <>
-                      {intl.formatMessage({
-                        id: 'pages.data.option.edit',
-                        defaultMessage: '编辑',
-                      })}
-                    </>
-                  ) : (
-                    <>
-                      {intl.formatMessage({
-                        id: 'pages.device.productDetail.base.save',
-                        defaultMessage: '保存',
-                      })}
-                    </>
-                  )}
-                </a>
-              }
-            >
-              <PreviewText.Placeholder value="-">
-                <Form form={form}>
-                  <FormLayout labelCol={6} wrapperCol={16}>
-                    <SchemaField schema={itemSchema} />
-                  </FormLayout>
-                </Form>
-              </PreviewText.Placeholder>
-            </Card>
-          </>
-        );
-      })
-    );
-  };
+  //       return (
+  //         <>
+  //           <Divider />
+  //           <Card
+  //             key={item.name}
+  //             title={item.name}
+  //             extra={
+  //               <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) {
+  //                       message.success('操作成功!');
+  //                       getDetailInfo();
+  //                     }
+  //                   }
+  //                   setState(!state);
+  //                 }}
+  //               >
+  //                 {state ? (
+  //                   <>
+  //                     {intl.formatMessage({
+  //                       id: 'pages.data.option.edit',
+  //                       defaultMessage: '编辑',
+  //                     })}
+  //                   </>
+  //                 ) : (
+  //                   <>
+  //                     {intl.formatMessage({
+  //                       id: 'pages.device.productDetail.base.save',
+  //                       defaultMessage: '保存',
+  //                     })}
+  //                   </>
+  //                 )}
+  //               </a>
+  //             }
+  //           >
+  //             <PreviewText.Placeholder value="-">
+  //               <Form form={form}>
+  //                 <FormLayout labelCol={6} wrapperCol={16}>
+  //                   <SchemaField schema={itemSchema} />
+  //                 </FormLayout>
+  //               </Form>
+  //             </PreviewText.Placeholder>
+  //           </Card>
+  //         </>
+  //       );
+  //     })
+  //   );
+  // };
 
   return (
     <>
@@ -231,7 +225,7 @@ const BaseInfo = () => {
         reload={getDetailInfo}
         visible={visible}
       />
-      {renderConfigCard()}
+      {/* {renderConfigCard()} */}
     </>
   );
 };

+ 32 - 13
src/pages/device/Product/Detail/index.tsx

@@ -10,7 +10,6 @@ import {
   Space,
   Spin,
   Switch,
-  Tabs,
   Tooltip,
 } from 'antd';
 import BaseInfo from '@/pages/device/Product/Detail/BaseInfo';
@@ -24,7 +23,6 @@ import Access from '@/pages/device/Product/Detail/Access';
 import type { DeviceMetadata } from '@/pages/device/Product/typings';
 import { Store } from 'jetlinks-store';
 import MetadataAction from '@/pages/device/components/Metadata/DataBaseAction';
-import { QuestionCircleOutlined } from '@ant-design/icons';
 import { getMenuPathByCode, MENUS_CODE } from '@/utils/menu';
 import encodeQuery from '@/utils/encodeQuery';
 import SystemConst from '@/utils/const';
@@ -148,11 +146,40 @@ const ProductDetail = observer(() => {
     return subscription.unsubscribe;
   }, [changeDeploy, param.id]);
 
+  const list = [
+    {
+      key: 'base',
+      tab: intl.formatMessage({
+        id: 'pages.device.productDetail.base',
+        defaultMessage: '配置信息',
+      }),
+      component: <BaseInfo />,
+    },
+    {
+      key: 'metadata',
+      tab: intl.formatMessage({
+        id: 'pages.device.productDetail.metadata',
+        defaultMessage: '物模型',
+      }),
+      component: <Metadata type="product" />,
+    },
+    {
+      key: 'access',
+      tab: '设备接入',
+      component: <Access />,
+    },
+  ];
+
   return (
     <PageContainer
       className={'page-title-show'}
       onBack={() => history.goBack()}
       extraContent={<Space size={24} />}
+      onTabChange={(key) => {
+        setMode(key);
+      }}
+      tabList={list}
+      tabActiveKey={mode}
       content={
         <Spin spinning={loading}>
           <Descriptions size="small" column={2}>
@@ -210,7 +237,8 @@ const ProductDetail = observer(() => {
       ]}
     >
       <Card>
-        <Tabs
+        {list.find((k) => k.key === mode)?.component}
+        {/* <Tabs
           defaultActiveKey={ModelEnum.base}
           activeKey={mode}
           onChange={(key) => {
@@ -273,16 +301,7 @@ const ProductDetail = observer(() => {
           <Tabs.TabPane tab={'设备接入'} key={ModelEnum.access}>
             <Access />
           </Tabs.TabPane>
-          {/* <Tabs.TabPane
-            tab={intl.formatMessage({
-              id: 'pages.device.productDetail.alarm',
-              defaultMessage: '告警设置',
-            })}
-            key="alarm"
-          >
-            <Alarm type="product" />
-          </Tabs.TabPane> */}
-        </Tabs>
+        </Tabs> */}
       </Card>
     </PageContainer>
   );

+ 42 - 17
src/pages/link/AccessConfig/Detail/Access/index.less

@@ -51,30 +51,55 @@
   }
 }
 
-.view {
+.search {
   display: flex;
+  align-items: center;
   justify-content: space-between;
+}
 
-  .info,
-  .config {
-    width: 48%;
+.info {
+  .title {
+    width: 100%;
+    margin-bottom: 10px;
+    color: rgba(0, 0, 0, 0.8);
+    font-weight: 600;
+  }
 
-    .title {
-      width: 100%;
-      margin-bottom: 10px;
-      font-weight: 600;
-    }
+  .title::before {
+    margin-right: 10px;
+    background-color: #2810ff;
+    content: '|';
+  }
+}
 
-    .title::before {
-      margin-right: 10px;
-      background-color: #2810ff;
-      content: '|';
+.config {
+  padding: 20px;
+  color: rgba(0, 0, 0, 0.8);
+  background: rgba(0, 0, 0, 0.04);
+  .title {
+    width: 100%;
+    margin-bottom: 10px;
+    font-weight: 600;
+  }
+  .item {
+    margin-bottom: 10px;
+    .context {
+      margin: 5px 0;
+      color: rgba(0, 0, 0, 0.8);
     }
   }
 }
 
-.search {
-  display: flex;
-  align-items: center;
-  justify-content: space-between;
+.alert {
+  height: 40px;
+  padding-left: 10px;
+  color: rgba(0, 0, 0, 0.55);
+  line-height: 40px;
+  background-color: #f6f6f6;
+}
+
+.cardRender {
+  width: 100%;
+  background: url('/images/access.png') no-repeat;
+  background-size: 100% 100%;
 }

+ 160 - 128
src/pages/link/AccessConfig/Detail/Access/index.tsx

@@ -1,5 +1,4 @@
 import {
-  Alert,
   Badge,
   Button,
   Card,
@@ -20,7 +19,7 @@ import encodeQuery from '@/utils/encodeQuery';
 import { useHistory, useLocation } from 'umi';
 import ReactMarkdown from 'react-markdown';
 import { getMenuPathByCode, MENUS_CODE } from '@/utils/menu';
-
+import { ExclamationCircleFilled } from '@ant-design/icons';
 interface Props {
   change: () => void;
   data: any;
@@ -291,7 +290,10 @@ const Access = (props: Props) => {
       case 0:
         return (
           <div>
-            <Alert message="选择与设备通信的网络组件" type="warning" showIcon />
+            <div className={styles.alert}>
+              <ExclamationCircleFilled style={{ marginRight: 10 }} />
+              选择与设备通信的网络组件
+            </div>
             <div className={styles.search}>
               <Input.Search
                 key={'network'}
@@ -328,34 +330,36 @@ const Access = (props: Props) => {
                 {networkList.map((item) => (
                   <Col key={item.name} span={8}>
                     <Card
+                      className={styles.cardRender}
                       style={{
                         width: '100%',
-                        border: networkCurrent === item.id ? '1px solid #1d39c4' : '',
+                        borderColor:
+                          networkCurrent === item.id ? 'var(--ant-primary-color-active)' : '',
                       }}
                       hoverable
                       onClick={() => {
                         setNetworkCurrent(item.id);
                       }}
                     >
-                      <div className={styles.title}>{item.name}</div>
+                      <div className={styles.title}>{item.name || '--'}</div>
                       <div className={styles.cardContent}>
                         <div
                           style={{
                             width: '100%',
-                            height: '40px',
+                            height: '20px',
                             display: 'flex',
                             flexDirection: 'column',
                             alignItems: 'center',
                             justifyContent: 'center',
                           }}
                         >
-                          {item.addresses.slice(0, 2).map((i: any) => (
+                          {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} />
                             </div>
                           ))}
                         </div>
-                        <div className={styles.desc}>{item?.description || ''}</div>
+                        <div className={styles.desc}>{item?.description || '--'}</div>
                       </div>
                     </Card>
                   </Col>
@@ -388,11 +392,10 @@ const Access = (props: Props) => {
       case 1:
         return (
           <div>
-            <Alert
-              message="使用选择的消息协议,对网络组件通信数据进行编解码、认证等操作"
-              type="warning"
-              showIcon
-            />
+            <div className={styles.alert}>
+              <ExclamationCircleFilled style={{ marginRight: 10 }} />
+              使用选择的消息协议,对网络组件通信数据进行编解码、认证等操作
+            </div>
             <div className={styles.search}>
               <Input.Search
                 key={'protocol'}
@@ -428,9 +431,11 @@ const Access = (props: Props) => {
                 {procotolList.map((item) => (
                   <Col key={item.name} span={8}>
                     <Card
+                      className={styles.cardRender}
                       style={{
                         width: '100%',
-                        border: procotolCurrent === item.id ? '1px solid #1d39c4' : '',
+                        borderColor:
+                          networkCurrent === item.id ? 'var(--ant-primary-color-active)' : '',
                       }}
                       hoverable
                       onClick={() => {
@@ -438,8 +443,8 @@ const Access = (props: Props) => {
                       }}
                     >
                       <div style={{ height: '45px' }}>
-                        <div className={styles.title}>{item.name}</div>
-                        <div className={styles.desc}>{item.description}</div>
+                        <div className={styles.title}>{item.name || '--'}</div>
+                        <div className={styles.desc}>{item.description || '--'}</div>
                       </div>
                     </Card>
                   </Col>
@@ -470,124 +475,151 @@ const Access = (props: Props) => {
         );
       case 2:
         return (
-          <div className={styles.view}>
-            <div className={styles.info}>
-              <div className={styles.title}>基本信息</div>
-              <Form name="basic" layout="vertical" form={form}>
-                <Form.Item
-                  label="名称"
-                  name="name"
-                  rules={[{ required: true, message: '请输入名称' }]}
-                >
-                  <Input />
-                </Form.Item>
-                <Form.Item name="description" label="说明">
-                  <Input.TextArea showCount maxLength={200} />
-                </Form.Item>
-              </Form>
-              <div className={styles.action}>
-                <Button style={{ margin: '0 8px' }} onClick={() => prev()}>
-                  上一步
-                </Button>
-                <Button
-                  type="primary"
-                  onClick={async () => {
-                    try {
-                      const values = await form.validateFields();
-                      // 编辑还是保存
-                      if (!params.get('id')) {
-                        service
-                          .save({
-                            name: values.name,
-                            description: values.description,
-                            provider: props.data.id,
-                            protocol: procotolCurrent,
-                            transport: ProcotoleMapping.get(props.data.id),
-                            channel: 'network', // 网络组件
-                            channelId: networkCurrent,
-                          })
-                          .then((resp: any) => {
-                            if (resp.status === 200) {
-                              message.success('操作成功!');
-                              history.goBack();
-                              if ((window as any).onTabSaveSuccess) {
-                                (window as any).onTabSaveSuccess(resp);
-                                setTimeout(() => window.close(), 300);
+          <Row gutter={24}>
+            <Col span={12}>
+              <div className={styles.info}>
+                <div className={styles.title}>基本信息</div>
+                <Form name="basic" layout="vertical" form={form}>
+                  <Form.Item
+                    label="名称"
+                    name="name"
+                    rules={[{ required: true, message: '请输入名称' }]}
+                  >
+                    <Input />
+                  </Form.Item>
+                  <Form.Item name="description" label="说明">
+                    <Input.TextArea showCount maxLength={200} />
+                  </Form.Item>
+                </Form>
+                <div className={styles.action} style={{ marginTop: 50 }}>
+                  <Button style={{ margin: '0 8px' }} onClick={() => prev()}>
+                    上一步
+                  </Button>
+                  <Button
+                    type="primary"
+                    onClick={async () => {
+                      try {
+                        const values = await form.validateFields();
+                        // 编辑还是保存
+                        if (!params.get('id')) {
+                          service
+                            .save({
+                              name: values.name,
+                              description: values.description,
+                              provider: props.data.id,
+                              protocol: procotolCurrent,
+                              transport: ProcotoleMapping.get(props.data.id),
+                              channel: 'network', // 网络组件
+                              channelId: networkCurrent,
+                            })
+                            .then((resp: any) => {
+                              if (resp.status === 200) {
+                                message.success('操作成功!');
+                                history.goBack();
+                                if ((window as any).onTabSaveSuccess) {
+                                  (window as any).onTabSaveSuccess(resp);
+                                  setTimeout(() => window.close(), 300);
+                                }
                               }
-                            }
-                          });
-                      } else {
-                        service
-                          .update({
-                            id: access?.id,
-                            name: values.name,
-                            description: values.description,
-                            provider: access?.provider,
-                            protocol: procotolCurrent,
-                            transport: access?.transport,
-                            channel: 'network', // 网络组件
-                            channelId: networkCurrent,
-                          })
-                          .then((resp: any) => {
-                            if (resp.status === 200) {
-                              message.success('操作成功!');
-                              history.goBack();
-                              if ((window as any).onTabSaveSuccess) {
-                                (window as any).onTabSaveSuccess(resp);
-                                setTimeout(() => window.close(), 300);
+                            });
+                        } else {
+                          service
+                            .update({
+                              id: access?.id,
+                              name: values.name,
+                              description: values.description,
+                              provider: access?.provider,
+                              protocol: procotolCurrent,
+                              transport: access?.transport,
+                              channel: 'network', // 网络组件
+                              channelId: networkCurrent,
+                            })
+                            .then((resp: any) => {
+                              if (resp.status === 200) {
+                                message.success('操作成功!');
+                                history.goBack();
+                                if ((window as any).onTabSaveSuccess) {
+                                  (window as any).onTabSaveSuccess(resp);
+                                  setTimeout(() => window.close(), 300);
+                                }
                               }
-                            }
-                          });
+                            });
+                        }
+                      } catch (errorInfo) {
+                        console.error('Failed:', errorInfo);
                       }
-                    } catch (errorInfo) {
-                      console.error('Failed:', errorInfo);
-                    }
-                  }}
-                >
-                  保存
-                </Button>
-              </div>
-            </div>
-            <div className={styles.config}>
-              <div className={styles.title}>接入方式</div>
-              <div>
-                {props.data?.name || providers.find((i) => i.id === access?.provider)?.name}
+                    }}
+                  >
+                    保存
+                  </Button>
+                </div>
               </div>
-              {(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 className={styles.title}>消息协议</div>
-              <div>{procotolList.find((i) => i.id === procotolCurrent)?.name || ''}</div>
-              <ReactMarkdown>{config?.document}</ReactMarkdown>
-              <div className={styles.title}>网络组件</div>
-              {(networkList.find((i) => i.id === networkCurrent)?.addresses || []).map(
-                (item: any) => (
-                  <div key={item.address}>
-                    <Badge
-                      color={item.health === -1 ? 'red' : 'green'}
-                      text={item.address}
-                      style={{ marginLeft: '20px' }}
-                    />
+            </Col>
+            <Col span={12}>
+              <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>
-                ),
-              )}
-              {config?.routes && config?.routes?.length > 0 && (
-                <div>
-                  <Table
-                    bordered
-                    dataSource={config?.routes || []}
-                    columns={config.id === 'MQTT' ? columnsMQTT : columnsHTTP}
-                    pagination={false}
-                    scroll={{ y: 240 }}
-                  />
                 </div>
-              )}
-            </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>
+                </div>
+                <div className={styles.item}>
+                  <div className={styles.title}>网络组件</div>
+                  {(networkList.find((i) => i.id === networkCurrent)?.addresses || []).length > 0
+                    ? (networkList.find((i) => i.id === networkCurrent)?.addresses || []).map(
+                        (item: any) => (
+                          <div key={item.address}>
+                            <Badge
+                              color={item.health === -1 ? 'red' : 'green'}
+                              text={item.address}
+                              style={{ marginLeft: '20px' }}
+                            />
+                          </div>
+                        ),
+                      )
+                    : '--'}
+                </div>
+                {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'
+                        ? 'topic'
+                        : 'URL信息'}
+                    </div>
+                    <Table
+                      bordered
+                      dataSource={config?.routes || []}
+                      columns={config.id === 'MQTT' ? columnsMQTT : columnsHTTP}
+                      pagination={false}
+                      scroll={{ y: 300 }}
+                    />
+                  </div>
+                )}
+              </div>
+            </Col>
+          </Row>
         );
       default:
         return null;

+ 74 - 0
src/pages/link/AccessConfig/Detail/Media/index.less

@@ -0,0 +1,74 @@
+.box {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  margin: 20px 30px;
+}
+
+.steps {
+  width: 100%;
+}
+
+.content {
+  width: 100%;
+  margin: 20px 0;
+}
+
+.action {
+  width: 100%;
+}
+
+.title {
+  width: '100%';
+  overflow: hidden;
+  font-weight: 800;
+  white-space: nowrap;
+  text-overflow: ellipsis;
+}
+
+.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;
+}
+
+.cardContent {
+  display: flex;
+  flex-direction: column;
+  margin-top: 5px;
+  color: rgba(0, 0, 0, 0.55);
+
+  .item {
+    width: 100%;
+    overflow: hidden;
+    white-space: nowrap;
+    text-overflow: ellipsis;
+  }
+}
+
+.view {
+  display: flex;
+  justify-content: space-between;
+
+  .info,
+  .config {
+    width: 48%;
+
+    .title {
+      width: 100%;
+      margin-bottom: 10px;
+      font-weight: 600;
+    }
+
+    .title::before {
+      margin-right: 10px;
+      background-color: #2810ff;
+      content: '|';
+    }
+  }
+}

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

@@ -0,0 +1,384 @@
+import { Alert, Button, Card, Form, Input, Steps } from 'antd';
+import { useEffect, useState } from 'react';
+import styles from './index.less';
+import {
+  ArrayCollapse,
+  Form as AForm,
+  FormButtonGroup,
+  FormCollapse,
+  FormGrid,
+  FormItem,
+  Input as AInput,
+  Radio,
+  Select,
+} from '@formily/antd';
+import { createSchemaField } from '@formily/react';
+import type { ISchema } from '@formily/json-schema';
+import { createForm } from '@formily/core';
+
+interface Props {
+  change: () => void;
+  data: any;
+}
+
+const Media = (props: Props) => {
+  const [current, setCurrent] = useState<number>(0);
+
+  const steps = [
+    {
+      title: '信令配置',
+    },
+    {
+      title: '完成',
+    },
+  ];
+
+  const BasicRender = () => {
+    const SchemaField = createSchemaField({
+      components: {
+        FormItem,
+        AInput,
+        Select,
+        Radio,
+        FormGrid,
+        FormCollapse,
+        ArrayCollapse,
+      },
+    });
+
+    const aform = createForm({});
+
+    const clusterConfig: ISchema = {
+      type: 'void',
+      'x-component': 'FormGrid',
+      'x-component-props': {
+        maxColumns: 3,
+        minColumns: 1,
+        columnGap: 48,
+      },
+      properties: {
+        serverId: {
+          title: '节点名称',
+          'x-component': 'AInput',
+          'x-decorator': 'FormItem',
+          'x-decorator-props': {
+            gridSpan: 1,
+            labelAlign: 'left',
+            layout: 'vertical',
+          },
+        },
+        host: {
+          title: 'SIP 地址',
+          'x-decorator': 'FormItem',
+          'x-component': 'AInput',
+          'x-decorator-props': {
+            gridSpan: 1,
+            labelAlign: 'left',
+            layout: 'vertical',
+            tooltip: '绑定到服务器上的网卡地址,绑定到所有网卡:0.0.0.0',
+          },
+          required: true,
+          'x-validator': ['ipv4'],
+        },
+        publicHost: {
+          title: '公网 Host',
+          'x-decorator-props': {
+            gridSpan: 1,
+            labelAlign: 'left',
+            tooltip: '监听指定端口的请求',
+            layout: 'vertical',
+          },
+          required: true,
+          type: 'number',
+          'x-decorator': 'FormItem',
+          'x-component': 'AInput',
+          'x-validator': [
+            {
+              max: 65535,
+              message: '请输入1-65535之间的整整数',
+            },
+            {
+              min: 1,
+              message: '请输入1-65535之间的整整数',
+            },
+          ],
+        },
+      },
+    };
+
+    const schema: ISchema = {
+      type: 'object',
+      properties: {
+        grid: {
+          type: 'void',
+          'x-component': 'FormGrid',
+          'x-component-props': {
+            maxColumns: 2,
+            minColumns: 1,
+            columnGap: 48,
+          },
+          properties: {
+            name: {
+              title: '名称',
+              type: 'string',
+              'x-decorator': 'FormItem',
+              'x-component': 'AInput',
+              'x-decorator-props': {
+                gridSpan: 1,
+              },
+              'x-validator': [
+                {
+                  max: 64,
+                  message: '最多可输入64个字符',
+                },
+                {
+                  required: true,
+                  message: '请输入名称',
+                },
+              ],
+            },
+            domain: {
+              title: 'SIP 域',
+              type: 'string',
+              'x-decorator': 'FormItem',
+              'x-component': 'AInput',
+              'x-decorator-props': {
+                gridSpan: 1,
+              },
+              'x-validator': [
+                {
+                  required: true,
+                  message: '请输入SIP域',
+                },
+              ],
+            },
+            sipId: {
+              title: 'SIP ID',
+              type: 'string',
+              'x-decorator': 'FormItem',
+              'x-component': 'AInput',
+              'x-decorator-props': {
+                gridSpan: 2,
+              },
+              'x-validator': [
+                {
+                  required: true,
+                  message: 'SIP ID',
+                },
+              ],
+            },
+            shareCluster: {
+              title: '集群',
+              'x-decorator': 'FormItem',
+              'x-component': 'Radio.Group',
+              required: true,
+              default: true,
+              enum: [
+                { label: '共享配置', value: true },
+                { label: '独立配置', value: false },
+              ],
+              'x-decorator-props': {
+                gridSpan: 2,
+                tooltip:
+                  '共享配置:集群下所有节点共用同一配置\r\n' + '独立配置:集群下不同节点使用不同配置',
+              },
+            },
+            hostPort: {
+              type: 'object',
+              'x-decorator': 'FormItem',
+              'x-reactions': [
+                {
+                  dependencies: ['.shareCluster'],
+                  fulfill: {
+                    state: {
+                      visible: '{{$deps[0]===true}}',
+                    },
+                  },
+                },
+              ],
+              'x-decorator-props': {
+                gridSpan: 2,
+              },
+              properties: {
+                grid: {
+                  type: 'void',
+                  'x-component': 'FormGrid',
+                  'x-component-props': {
+                    maxColumns: 2,
+                    minColumns: 1,
+                    columnGap: 48,
+                  },
+                  properties: {
+                    host: {
+                      title: 'SIP 地址',
+                      'x-component': 'AInput',
+                      'x-decorator': 'FormItem',
+                      'x-decorator-props': {
+                        gridSpan: 1,
+                        labelAlign: 'left',
+                        layout: 'vertical',
+                      },
+                    },
+                    publicHost: {
+                      title: '公网 Host',
+                      'x-component': 'AInput',
+                      'x-decorator': 'FormItem',
+                      'x-decorator-props': {
+                        gridSpan: 1,
+                        labelAlign: 'left',
+                        layout: 'vertical',
+                      },
+                    },
+                  },
+                },
+              },
+            },
+            cluster: {
+              type: 'void',
+              'x-decorator': 'FormItem',
+              'x-decorator-props': {
+                gridSpan: 3,
+              },
+              'x-reactions': {
+                dependencies: ['.shareCluster'],
+                fulfill: {
+                  state: {
+                    visible: '{{$deps[0]===false}}',
+                  },
+                },
+              },
+              'x-visible': false,
+              properties: {
+                cluster: {
+                  type: 'array',
+                  'x-component': 'ArrayCollapse',
+                  'x-decorator': 'FormItem',
+                  items: {
+                    type: 'void',
+                    'x-component': 'ArrayCollapse.CollapsePanel',
+                    'x-component-props': {
+                      header: '节点',
+                    },
+                    properties: {
+                      index: {
+                        type: 'void',
+                        'x-component': 'ArrayCollapse.Index',
+                      },
+                      layout2: clusterConfig,
+                      remove: {
+                        type: 'void',
+                        'x-component': 'ArrayCollapse.Remove',
+                      },
+                    },
+                  },
+                  properties: {
+                    addition: {
+                      type: 'void',
+                      title: '新增',
+                      'x-component': 'ArrayCollapse.Addition',
+                    },
+                  },
+                },
+              },
+            },
+          },
+        },
+      },
+    };
+
+    return (
+      <div>
+        <Alert message="配置设备信令参数" type="warning" showIcon />
+        <AForm form={aform} layout="vertical" style={{ padding: 30 }}>
+          <SchemaField schema={schema} />
+          <FormButtonGroup.Sticky>
+            <FormButtonGroup.FormItem>
+              <Button
+                onClick={() => {
+                  setCurrent(1);
+                }}
+              >
+                下一步
+              </Button>
+            </FormButtonGroup.FormItem>
+          </FormButtonGroup.Sticky>
+        </AForm>
+      </div>
+    );
+  };
+
+  const FinishRender = () => {
+    const [form] = Form.useForm();
+    return (
+      <div className={styles.view}>
+        <div className={styles.info}>
+          <div className={styles.title}>基本信息</div>
+          <Form name="basic" layout="vertical" form={form}>
+            <Form.Item label="名称" name="name" rules={[{ required: true, message: '请输入名称' }]}>
+              <Input />
+            </Form.Item>
+            <Form.Item name="description" label="说明">
+              <Input.TextArea showCount maxLength={200} />
+            </Form.Item>
+          </Form>
+          <div className={styles.action}>
+            {props.data.id !== 'fixed-media' && (
+              <Button
+                style={{ margin: '0 8px' }}
+                onClick={() => {
+                  setCurrent(0);
+                }}
+              >
+                上一步
+              </Button>
+            )}
+            <Button type="primary" onClick={() => {}}>
+              保存
+            </Button>
+          </div>
+        </div>
+        <div className={styles.config}>
+          <div className={styles.title}>接入方式</div>
+          <div>这里是接入方式说明</div>
+          <div className={styles.title}>消息协议</div>
+          <div>这里是接入方式说明</div>
+        </div>
+      </div>
+    );
+  };
+
+  useEffect(() => {
+    console.log(props.data);
+  }, []);
+
+  return (
+    <Card>
+      {props.data?.id && (
+        <Button
+          type="link"
+          onClick={() => {
+            props.change();
+          }}
+        >
+          返回
+        </Button>
+      )}
+      {props.data.id === 'fixed-media' ? (
+        FinishRender()
+      ) : (
+        <div className={styles.box}>
+          <div className={styles.steps}>
+            <Steps size="small" current={current}>
+              {steps.map((item) => (
+                <Steps.Step key={item.title} title={item.title} />
+              ))}
+            </Steps>
+          </div>
+          <div className={styles.content}>{current === 0 ? BasicRender() : FinishRender()}</div>
+        </div>
+      )}
+    </Card>
+  );
+};
+
+export default Media;

+ 14 - 0
src/pages/link/AccessConfig/Detail/Provider/index.less

@@ -25,3 +25,17 @@
   white-space: nowrap;
   text-overflow: ellipsis;
 }
+
+.title {
+  width: '100%';
+  margin-bottom: 10px;
+  overflow: hidden;
+  font-weight: 800;
+  white-space: nowrap;
+  text-overflow: ellipsis;
+}
+.title::before {
+  margin-right: 10px;
+  background-color: #2810ff;
+  content: '|';
+}

+ 62 - 10
src/pages/link/AccessConfig/Detail/Provider/index.tsx

@@ -1,19 +1,30 @@
-import { Button, Card, Col, Empty, Row } from 'antd';
+import { Button, Card, Col, Row } from 'antd';
 import { service } from '@/pages/link/AccessConfig';
 import { useEffect, useState } from 'react';
 import styles from './index.less';
 
 interface Props {
-  change: (id: string) => void;
+  change: (data: any, type: 'media' | 'network') => void;
 }
 
 const Provider = (props: Props) => {
   const [dataSource, setDataSource] = useState<any[]>([]);
+  const [mediaSource, setMediaSource] = useState<any[]>([]);
 
   const handleSearch = () => {
     service.getProviders().then((resp) => {
       if (resp.status === 200) {
-        setDataSource(resp.result);
+        const media: any[] = [];
+        const data: any = [];
+        resp.result.map((item: any) => {
+          if (item.id === 'fixed-media' || item.id === 'gb28181-2016') {
+            media.push(item);
+          } else {
+            data.push(item);
+          }
+        });
+        setDataSource(data);
+        setMediaSource(media);
       }
     });
   };
@@ -23,8 +34,9 @@ const Provider = (props: Props) => {
   }, []);
 
   return (
-    <Card style={{ padding: '20px' }}>
-      {dataSource.length > 0 ? (
+    <>
+      <Card>
+        <div className={styles.title}>自定义设备接入</div>
         <Row gutter={[16, 16]}>
           {dataSource.map((item) => (
             <Col key={item.name} span={12}>
@@ -53,7 +65,7 @@ const Provider = (props: Props) => {
                     <Button
                       type="primary"
                       onClick={() => {
-                        props.change(item);
+                        props.change(item, 'network');
                       }}
                     >
                       接入
@@ -64,10 +76,50 @@ const Provider = (props: Props) => {
             </Col>
           ))}
         </Row>
-      ) : (
-        <Empty />
-      )}
-    </Card>
+      </Card>
+      <Card style={{ marginTop: 20 }}>
+        <div className={styles.title}>视频类设备接入</div>
+        <Row gutter={[16, 16]}>
+          {mediaSource.map((item) => (
+            <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 style={{ fontWeight: 600 }}>{item.name}</div>
+                      <div className={styles.desc}>{item.description}</div>
+                    </div>
+                  </div>
+                  <div style={{ width: '70px' }}>
+                    <Button
+                      type="primary"
+                      onClick={() => {
+                        props.change(item, 'media');
+                      }}
+                    >
+                      接入
+                    </Button>
+                  </div>
+                </div>
+              </Card>
+            </Col>
+          ))}
+        </Row>
+      </Card>
+    </>
   );
 };
 

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

@@ -3,6 +3,7 @@ import { useState } from 'react';
 import { useLocation } from 'umi';
 import Access from './Access';
 import Provider from './Provider';
+import Media from './Media';
 
 type LocationType = {
   id?: string;
@@ -12,23 +13,43 @@ const Detail = () => {
   const location = useLocation<LocationType>();
   const [visible, setVisible] = useState<boolean>(!new URLSearchParams(location.search).get('id'));
   const [data, setData] = useState<any>({});
+  const [type, setType] = useState<'media' | 'network'>('media');
+
+  const componentRender = () => {
+    switch (type) {
+      case 'network':
+        return (
+          <Access
+            data={data}
+            change={() => {
+              setVisible(true);
+            }}
+          />
+        );
+      case 'media':
+        return (
+          <Media
+            data={data}
+            change={() => {
+              setVisible(true);
+            }}
+          />
+        );
+    }
+  };
 
   return (
-    <PageContainer className={'page-title-show'}>
+    <PageContainer>
       {visible ? (
         <Provider
-          change={(param: any) => {
+          change={(param: any, typings: 'media' | 'network') => {
+            setType(typings);
             setData(param);
             setVisible(false);
           }}
         />
       ) : (
-        <Access
-          data={data}
-          change={() => {
-            setVisible(true);
-          }}
-        />
+        componentRender()
       )}
     </PageContainer>
   );

+ 87 - 67
src/pages/link/AccessConfig/index.less

@@ -1,80 +1,100 @@
-.box {
-  display: flex;
-  justify-content: space-between;
-}
+// .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%;
-}
+// .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);
-}
+// .content {
+//   display: flex;
+//   flex-direction: column;
+//   width: calc(100% - 80px);
+// }
 
-.top {
-  display: flex;
-  justify-content: space-between;
+// .top {
+//   display: flex;
+//   justify-content: space-between;
 
-  .left {
-    display: flex;
-  }
+//   .left {
+//     display: flex;
+//   }
 
-  .action a {
-    margin: 0 5px;
-  }
-}
+//   .action a {
+//     margin: 0 5px;
+//   }
+// }
 
-.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;
-}
-
-.title {
-  font-weight: 600;
-  font-size: 14px;
-}
-
-.container {
+.context {
   display: flex;
   width: 100%;
-  height: 90px;
-  margin-top: 10px;
-}
+  .card {
+    display: flex;
+    flex-direction: column;
+    width: 100%;
+    margin-left: 10px;
+    .header {
+      .title {
+        width: 90%;
+        overflow: hidden;
+        font-weight: 700;
+        font-size: 18px;
+        white-space: nowrap;
+        text-overflow: ellipsis;
+      }
+      .desc {
+        width: 100%;
+        margin-top: 10px;
+        overflow: hidden;
+        color: #666;
+        font-weight: 400;
+        font-size: 12px;
+        white-space: nowrap;
+        text-overflow: ellipsis;
+      }
+    }
 
-.server {
-  align-items: center;
-  width: 50%;
-  margin-right: 20px;
-}
+    .container {
+      display: flex;
+      width: 100%;
+      height: 80px;
+      margin-top: 10px;
 
-.procotol {
-  display: -webkit-box;
-  width: 50%;
-  overflow: hidden;
-  text-overflow: ellipsis;
-  -webkit-box-orient: vertical;
-  -webkit-line-clamp: 4;
+      .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 {

+ 144 - 135
src/pages/link/AccessConfig/index.tsx

@@ -1,13 +1,17 @@
+import { TableCard } from '@/components';
 import SearchComponent from '@/components/SearchComponent';
 import { getMenuPathByCode, MENUS_CODE } from '@/utils/menu';
-import { CheckCircleOutlined, DeleteOutlined, EditOutlined, StopOutlined } from '@ant-design/icons';
+import { StatusColorEnum } from '@/components/BadgeStatus';
 import { PageContainer } from '@ant-design/pro-layout';
 import type { ProColumns } from '@jetlinks/pro-table';
-import { Badge, Button, Card, Col, message, Pagination, Popconfirm, Row } from 'antd';
+import { Badge, 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');
 
 export const service = new Service('gateway/device');
 
@@ -53,7 +57,9 @@ const AccessConfig = () => {
     service
       .queryList({ ...params, sorts: [{ name: 'createTime', order: 'desc' }] })
       .then((resp) => {
-        setDataSource(resp.result);
+        if (resp.status === 200) {
+          setDataSource(resp.result);
+        }
       });
   };
 
@@ -75,9 +81,6 @@ const AccessConfig = () => {
             };
             handleSearch(dt);
           }}
-          // onReset={() => {
-          //   handleSearch({ pageSize: 10 });
-          // }}
         />
         <div style={{ width: '100%', display: 'flex', justifyContent: 'flex-end' }}>
           <Button
@@ -89,140 +92,146 @@ const AccessConfig = () => {
             新增
           </Button>
         </div>
-        <Row gutter={[16, 16]} style={{ marginTop: 10 }}>
-          {(dataSource?.data || []).map((item: any) => (
-            <Col key={item.name} span={12}>
-              <Card hoverable>
-                <div className={styles.box}>
-                  <div className={styles.images}>{item.name}</div>
-                  <div className={styles.content}>
-                    <div className={styles.header}>
-                      <div className={styles.top}>
-                        <div className={styles.left}>
-                          <div className={styles.title}>{item.name}</div>
-                          <div className={styles.status}>
-                            <Badge
-                              color={item.state.value === 'disabled' ? 'red' : 'green'}
-                              text={item.state.text}
-                              style={{ marginLeft: '20px' }}
-                            />
-                          </div>
-                        </div>
-                        <div className={styles.action}>
-                          <a
-                            key="edit"
-                            onClick={() => {
-                              history.push(
-                                `${getMenuPathByCode(MENUS_CODE['link/AccessConfig/Detail'])}?id=${
-                                  item.id
-                                }`,
-                              );
-                            }}
-                          >
-                            <EditOutlined />
-                            编辑
-                          </a>
-                          <a key="warning">
-                            <Popconfirm
-                              title={`确认${item.state.value !== 'disabled' ? '禁用' : '启用'}`}
-                              onConfirm={() => {
-                                if (item.state.value !== 'disabled') {
-                                  service.shutDown(item.id).then((resp) => {
-                                    if (resp.status === 200) {
-                                      message.success('操作成功!');
-                                      handleSearch(param);
-                                    }
-                                  });
-                                } else {
-                                  service.startUp(item.id).then((resp) => {
-                                    if (resp.status === 200) {
-                                      message.success('操作成功!');
-                                      handleSearch(param);
-                                    }
-                                  });
-                                }
-                              }}
-                            >
-                              {item.state.value !== 'disabled' ? (
-                                <span>
-                                  <StopOutlined />
-                                  禁用
-                                </span>
-                              ) : (
-                                <span>
-                                  <CheckCircleOutlined />
-                                  启用
-                                </span>
-                              )}
-                            </Popconfirm>
-                          </a>
-                          <a key="remove">
-                            <Popconfirm
-                              title={'确认删除?'}
-                              onConfirm={() => {
-                                service.remove(item.id).then((resp: any) => {
-                                  if (resp.status === 200) {
-                                    message.success('操作成功!');
-                                    handleSearch(param);
-                                  }
-                                });
-                              }}
-                            >
-                              <DeleteOutlined />
-                              删除
-                            </Popconfirm>
-                          </a>
-                        </div>
-                      </div>
-                      <div className={styles.desc}>{item.description}</div>
+        {dataSource?.data.length > 0 ? (
+          <Row gutter={[16, 16]} style={{ marginTop: 10 }}>
+            {(dataSource?.data || []).map((item: any) => (
+              <Col key={item.name} span={12}>
+                <TableCard
+                  showMask={false}
+                  actions={[
+                    <Button
+                      key="edit"
+                      type="link"
+                      onClick={() => {
+                        history.push(
+                          `${getMenuPathByCode(MENUS_CODE['link/AccessConfig/Detail'])}?id=${
+                            item.id
+                          }`,
+                        );
+                      }}
+                    >
+                      <EditOutlined />
+                      编辑
+                    </Button>,
+                    <Button key="warning" type="link">
+                      <Popconfirm
+                        title={`确认${item.state.value !== 'disabled' ? '禁用' : '启用'}`}
+                        onConfirm={() => {
+                          if (item.state.value !== 'disabled') {
+                            service.shutDown(item.id).then((resp) => {
+                              if (resp.status === 200) {
+                                message.success('操作成功!');
+                                handleSearch(param);
+                              }
+                            });
+                          } else {
+                            service.startUp(item.id).then((resp) => {
+                              if (resp.status === 200) {
+                                message.success('操作成功!');
+                                handleSearch(param);
+                              }
+                            });
+                          }
+                        }}
+                      >
+                        {item.state.value !== 'disabled' ? (
+                          <span>
+                            <StopOutlined />
+                            禁用
+                          </span>
+                        ) : (
+                          <span>
+                            <CheckCircleOutlined />
+                            启用
+                          </span>
+                        )}
+                      </Popconfirm>
+                    </Button>,
+                    <Button key="delete" type="link">
+                      <Popconfirm
+                        title={'确认删除?'}
+                        onConfirm={() => {
+                          service.remove(item.id).then((resp: any) => {
+                            if (resp.status === 200) {
+                              message.success('操作成功!');
+                              handleSearch(param);
+                            }
+                          });
+                        }}
+                      >
+                        <DeleteOutlined />
+                        删除
+                      </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.container}>
-                      <div className={styles.server}>
-                        <div className={styles.title}>{item?.channelInfo?.name}</div>
-                        <p>
-                          {item.channelInfo?.addresses.map((i: any) => (
-                            <div key={i.address}>
-                              <Badge color={i.health === -1 ? 'red' : 'green'} text={i.address} />
-                            </div>
-                          ))}
-                        </p>
+                    <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.procotol}>
-                        <div className={styles.title}>{item?.protocolDetail?.name}</div>
-                        <p style={{ color: 'rgba(0, 0, 0, .55)' }}>
-                          {item.protocolDetail?.description}
-                        </p>
+                      <div className={styles.container}>
+                        <div className={styles.server}>
+                          <div className={styles.subTitle}>{item?.channelInfo?.name || '--'}</div>
+                          <p>
+                            {item.channelInfo?.addresses.map((i: any) => (
+                              <div key={i.address}>
+                                <Badge color={i.health === -1 ? 'red' : 'green'} text={i.address} />
+                              </div>
+                            ))}
+                          </p>
+                        </div>
+                        <div className={styles.procotol}>
+                          <div className={styles.subTitle}>
+                            {item?.protocolDetail?.name || '--'}
+                          </div>
+                          <p>{item.protocolDetail?.description || '--'}</p>
+                        </div>
                       </div>
                     </div>
                   </div>
-                </div>
-              </Card>
-            </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={[10, 20, 50, 100]}
-            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>
+                </TableCard>
+              </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,
+                });
+              }}
+              pageSizeOptions={[10, 20, 50, 100]}
+              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>
+        )}
       </Card>
     </PageContainer>
   );

+ 46 - 26
src/pages/media/Stream/index.less

@@ -1,34 +1,54 @@
-.card {
-  .header {
-    display: flex;
-    align-items: center;
-    justify-content: space-between;
-    width: 100%;
+.cardRender {
+  width: 100%;
+  background: url('/images/access.png') no-repeat;
+  background-size: 100% 100%;
 
-    .title {
-      width: 50%;
-      overflow: hidden;
-      font-weight: 800;
-      white-space: nowrap;
-      text-overflow: ellipsis;
-    }
+  .card {
+    .header {
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+      width: 100%;
 
-    .title::before {
-      margin-right: 10px;
-      background-color: #2810ff;
-      content: '|';
-    }
-  }
+      .title {
+        width: 50%;
+        overflow: hidden;
+        color: rgba(0, 0, 0, 0.85);
+        font-weight: 700;
+        font-size: 16px;
+        white-space: nowrap;
+        text-overflow: ellipsis;
+      }
 
-  .content {
-    display: flex;
-    justify-content: space-between;
-    width: 100%;
-    margin-top: 10px;
+      .actions {
+        .action span {
+          margin-left: 5px;
+          color: #4f4f4f;
+        }
+      }
+    }
 
-    .item {
+    .content {
       display: flex;
-      flex-direction: column;
+      justify-content: space-between;
+      width: 100%;
+      margin-top: 20px;
+
+      .item {
+        display: flex;
+        flex-direction: column;
+
+        .itemTitle {
+          color: rgba(0, 0, 0, 0.45);
+          font-size: 12px;
+        }
+
+        p {
+          color: rgba(0, 0, 0, 0.75);
+          font-weight: 700;
+          font-size: 12px;
+        }
+      }
     }
   }
 }

+ 10 - 9
src/pages/media/Stream/index.tsx

@@ -92,13 +92,14 @@ const Stream = () => {
             <Row gutter={[16, 16]} style={{ marginTop: 10 }}>
               {(dataSource?.data || []).map((item: any) => (
                 <Col key={item.id} span={12}>
-                  <Card hoverable>
+                  <Card hoverable className={styles.cardRender}>
                     <div className={styles.card}>
                       <div className={styles.header}>
                         <div className={styles.title}>{item?.name}</div>
                         <div className={styles.actions}>
                           <Space>
-                            <a
+                            <span
+                              className={styles.action}
                               onClick={() => {
                                 history.push(
                                   `${getMenuPathByParams(
@@ -109,9 +110,9 @@ const Stream = () => {
                                 StreamModel.current = { ...item };
                               }}
                             >
-                              <EditOutlined />
-                              编辑
-                            </a>
+                              <EditOutlined style={{ color: '#000000' }} />
+                              <span>编辑</span>
+                            </span>
                             <Popconfirm
                               title={'确认删除?'}
                               onConfirm={() => {
@@ -123,10 +124,10 @@ const Stream = () => {
                                 });
                               }}
                             >
-                              <a>
-                                <DeleteOutlined />
-                                删除
-                              </a>
+                              <span className={styles.action}>
+                                <DeleteOutlined style={{ color: '#E50012' }} />
+                                <span>删除</span>
+                              </span>
                             </Popconfirm>
                           </Space>
                         </div>

+ 3 - 3
src/pages/rule-engine/Instance/index.tsx

@@ -245,13 +245,13 @@ const Instance = () => {
         <Popconfirm
           title={intl.formatMessage({
             id:
-              record.state.value === 'notActive'
+              record.state.value === 'stopped'
                 ? 'pages.data.option.remove.tips'
                 : 'pages.device.instance.deleteTip',
           })}
           key={'delete'}
           onConfirm={async () => {
-            if (record.state.value === 'notActive') {
+            if (record.state.value === 'stopped') {
               await service.remove(record.id);
               message.success(
                 intl.formatMessage({
@@ -261,7 +261,7 @@ const Instance = () => {
               );
               actionRef.current?.reload();
             } else {
-              message.error(intl.formatMessage({ id: 'pages.device.instance.deleteTip' }));
+              message.error('未停止不能删除');
             }
           }}
         >