Explorar el Código

fix(device): device \link

Lind hace 3 años
padre
commit
f5ed27b014

+ 52 - 0
src/components/ProTableCard/CardItems/product.tsx

@@ -0,0 +1,52 @@
+import { Avatar, Card } from 'antd';
+import React from 'react';
+import type { ProductItem } from '@/pages/device/Product/typings';
+import { BadgeStatus } from '@/components';
+import { StatusColorEnum } from '@/components/BadgeStatus';
+import '@/style/common.less';
+import { useIntl } from '@@/plugin-locale/localeExports';
+
+export interface ProductCardProps extends ProductItem {
+  actions?: React.ReactNode[];
+  avatarSize?: number;
+}
+
+export default (props: ProductCardProps) => {
+  const intl = useIntl();
+
+  return (
+    <Card style={{ width: 340 }} cover={null} actions={props.actions}>
+      <div className={'pro-table-card-item'}>
+        <div className={'card-item-avatar'}>
+          <Avatar size={props.avatarSize || 64} src={props.photoUrl} />
+        </div>
+        <div className={'card-item-body'}>
+          <div className={'card-item-header'}>
+            <span className={'card-item-header-name ellipsis'}>{props.name}</span>
+            <BadgeStatus
+              status={props.state}
+              text={intl.formatMessage({
+                id: `pages.system.tenant.assetInformation.${
+                  props.state ? 'published' : 'unpublished'
+                }`,
+                defaultMessage: '已发布',
+              })}
+              statusNames={{
+                0: StatusColorEnum.error,
+                1: StatusColorEnum.processing,
+              }}
+            />
+          </div>
+          <div className={'card-item-content'}>
+            <label>设备类型:</label>
+            <span className={'ellipsis'}>{props.deviceType ? props.deviceType.text : '--'}</span>
+          </div>
+          <div className={'card-item-content'}>
+            <label>接入方式:</label>
+            <span className={'ellipsis'}>{props.transportProtocol || '--'}</span>
+          </div>
+        </div>
+      </div>
+    </Card>
+  );
+};

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

@@ -41,13 +41,19 @@ const ProTableCard = <
    */
    */
   const handleCard = (dataSource: readonly T[] | undefined): JSX.Element => {
   const handleCard = (dataSource: readonly T[] | undefined): JSX.Element => {
     return (
     return (
-      <div className={'pro-table-card-items'}>
-        {dataSource ? (
-          dataSource.map((item) => (cardRender && isFunction(cardRender) ? cardRender(item) : null))
+      <>
+        {dataSource && dataSource.length ? (
+          <div className={'pro-table-card-items'}>
+            {dataSource.map((item) =>
+              cardRender && isFunction(cardRender) ? cardRender(item) : null,
+            )}
+          </div>
         ) : (
         ) : (
-          <Empty />
+          <div style={{ display: 'flex', justifyContent: 'center' }}>
+            <Empty />
+          </div>
         )}
         )}
-      </div>
+      </>
     );
     );
   };
   };
 
 

+ 4 - 2
src/locales/zh-CN/pages.ts

@@ -190,9 +190,9 @@ export default {
   'pages.device.productDetail.id': '产品ID',
   'pages.device.productDetail.id': '产品ID',
   'pages.device.productDetail.classifiedName': '所属品类',
   'pages.device.productDetail.classifiedName': '所属品类',
   'pages.device.productDetail.protocolName': '消息协议',
   'pages.device.productDetail.protocolName': '消息协议',
-  'pages.device.productDetail.transportProtocol': '链接协议',
+  'pages.device.productDetail.transportProtocol': '接入方式',
   'pages.device.productDetail.createTime': '创建时间',
   'pages.device.productDetail.createTime': '创建时间',
-  'pages.device.productDetail.updateTime': '创建时间',
+  'pages.device.productDetail.updateTime': '更新时间',
   'pages.device.productDetail.base': '配置信息',
   'pages.device.productDetail.base': '配置信息',
   'pages.device.productDetail.base.save': '保存',
   'pages.device.productDetail.base.save': '保存',
   'pages.device.productDetail.metadata': '物模型',
   'pages.device.productDetail.metadata': '物模型',
@@ -231,6 +231,7 @@ export default {
   'pages.device.productDetail.setting': '应用配置',
   'pages.device.productDetail.setting': '应用配置',
   'pages.device.productDetail.disable': '停用',
   'pages.device.productDetail.disable': '停用',
   'pages.device.productDetail.enabled': '启用',
   'pages.device.productDetail.enabled': '启用',
+  'pages.device.productDetail.deleteTip': '已发布的产品不能进行删除操作',
 
 
   // 设备管理-设备分类
   // 设备管理-设备分类
   'pages.device.type.device': '直连设备',
   'pages.device.type.device': '直连设备',
@@ -250,6 +251,7 @@ export default {
   'pages.device.instance.status.notActive': '未启用',
   'pages.device.instance.status.notActive': '未启用',
   'pages.device.instance.status.offLine': '离线',
   'pages.device.instance.status.offLine': '离线',
   'pages.device.instance.status.onLine': '在线',
   'pages.device.instance.status.onLine': '在线',
+  'pages.device.instance.deleteTip': '已启用的设备无法删除',
   'pages.device.instanceDetail.deviceType': '设备类型',
   'pages.device.instanceDetail.deviceType': '设备类型',
   'pages.device.instanceDetail.transportProtocol': '链接协议',
   'pages.device.instanceDetail.transportProtocol': '链接协议',
   'pages.device.instanceDetail.protocolName': '消息协议',
   'pages.device.instanceDetail.protocolName': '消息协议',

+ 11 - 9
src/pages/device/Instance/Detail/Config/index.tsx

@@ -108,16 +108,18 @@ const Config = () => {
                 <Space>
                 <Space>
                   <a
                   <a
                     onClick={async () => {
                     onClick={async () => {
-                      const values = (await form.submit()) as any;
-                      const resp = await service.modify(id || '', {
-                        id,
-                        configuration: { ...values },
-                      });
-                      if (resp.status === 200) {
-                        InstanceModel.detail = {
-                          ...InstanceModel.detail,
+                      if (!state) {
+                        const values = (await form.submit()) as any;
+                        const resp = await service.modify(id || '', {
+                          id,
                           configuration: { ...values },
                           configuration: { ...values },
-                        };
+                        });
+                        if (resp.status === 200) {
+                          InstanceModel.detail = {
+                            ...InstanceModel.detail,
+                            configuration: { ...values },
+                          };
+                        }
                         setState(!state);
                         setState(!state);
                       }
                       }
                     }}
                     }}

+ 1 - 0
src/pages/device/Instance/Save/index.tsx

@@ -76,6 +76,7 @@ const Save = (props: Props) => {
           props.reload();
           props.reload();
         }
         }
         props.close(resp.result);
         props.close(resp.result);
+        form.resetFields();
       }
       }
     }
     }
   };
   };

+ 52 - 39
src/pages/device/Instance/index.tsx

@@ -83,42 +83,46 @@ const Instance = () => {
         <EyeOutlined />
         <EyeOutlined />
       </Tooltip>
       </Tooltip>
     </Link>,
     </Link>,
-    <a href={record.id} target="_blank" rel="noopener noreferrer" key="view">
-      <Popconfirm
+    <Popconfirm
+      title={intl.formatMessage({
+        id: `pages.data.option.${record.state.value !== 'notActive' ? 'disabled' : 'enabled'}.tips`,
+        defaultMessage: '确认禁用?',
+      })}
+      onConfirm={async () => {
+        if (record.state.value !== 'notActive') {
+          await service.undeployDevice(record.id);
+        } else {
+          await service.deployDevice(record.id);
+        }
+        message.success(
+          intl.formatMessage({
+            id: 'pages.data.option.success',
+            defaultMessage: '操作成功!',
+          }),
+        );
+        actionRef.current?.reload();
+      }}
+    >
+      <Tooltip
         title={intl.formatMessage({
         title={intl.formatMessage({
-          id: 'pages.data.option.disabled.tips',
-          defaultMessage: '确认禁用?',
+          id: `pages.data.option.${record.state.value !== 'notActive' ? 'disabled' : 'enabled'}`,
+          defaultMessage: record.state.value !== 'notActive' ? '禁用' : '启用',
         })}
         })}
-        onConfirm={async () => {
-          if (record.state.value !== 'notActive') {
-            await service.undeployDevice(record.id);
-          } else {
-            await service.deployDevice(record.id);
-          }
-          message.success(
-            intl.formatMessage({
-              id: 'pages.data.option.success',
-              defaultMessage: '操作成功!',
-            }),
-          );
-          actionRef.current?.reload();
-        }}
       >
       >
-        <Tooltip
-          title={intl.formatMessage({
-            id: `pages.data.option.${record.state.value !== 'notActive' ? 'disabled' : 'enabled'}`,
-            defaultMessage: record.state.value !== 'notActive' ? '禁用' : '启用',
-          })}
-        >
+        <Button type={'link'}>
           {record.state.value !== 'notActive' ? <StopOutlined /> : <CheckCircleOutlined />}
           {record.state.value !== 'notActive' ? <StopOutlined /> : <CheckCircleOutlined />}
-        </Tooltip>
-      </Popconfirm>
-    </a>,
-
-    <a key={'delete'}>
-      <Popconfirm
-        title="确认删除"
-        onConfirm={async () => {
+        </Button>
+      </Tooltip>
+    </Popconfirm>,
+    <Popconfirm
+      title={intl.formatMessage({
+        id:
+          record.state.value === 'notActive'
+            ? 'pages.data.option.remove.tips'
+            : 'pages.device.instance.deleteTip',
+      })}
+      onConfirm={async () => {
+        if (record.state.value === 'notActive') {
           await service.remove(record.id);
           await service.remove(record.id);
           message.success(
           message.success(
             intl.formatMessage({
             intl.formatMessage({
@@ -127,13 +131,22 @@ const Instance = () => {
             }),
             }),
           );
           );
           actionRef.current?.reload();
           actionRef.current?.reload();
-        }}
+        } else {
+          message.error(intl.formatMessage({ id: 'pages.device.instance.deleteTip' }));
+        }
+      }}
+    >
+      <Tooltip
+        title={intl.formatMessage({
+          id: 'pages.data.option.remove',
+          defaultMessage: '删除',
+        })}
       >
       >
-        <Tooltip title={'删除'}>
+        <Button type={'link'}>
           <DeleteOutlined />
           <DeleteOutlined />
-        </Tooltip>
-      </Popconfirm>
-    </a>,
+        </Button>
+      </Tooltip>
+    </Popconfirm>,
   ];
   ];
 
 
   const columns: ProColumns<DeviceInstance>[] = [
   const columns: ProColumns<DeviceInstance>[] = [
@@ -349,8 +362,8 @@ const Instance = () => {
             ...params,
             ...params,
             sorts: [
             sorts: [
               {
               {
-                name: 'id',
-                order: 'ascend',
+                name: 'createTime',
+                order: 'desc',
               },
               },
             ],
             ],
           })
           })

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

@@ -0,0 +1,198 @@
+import { useEffect, useState } from 'react';
+import { Badge, Button, Card, 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 Service from '@/pages/device/Product/service';
+
+interface Props {
+  close: () => void;
+  data?: any;
+}
+
+const AccessConfig = (props: Props) => {
+  const { close } = props;
+  const service1 = new Service('device-product');
+
+  const [dataSource, setDataSource] = useState<any>({
+    data: [],
+    pageSize: 4,
+    pageIndex: 0,
+    total: 0,
+  });
+  const [param, setParam] = useState<any>({ pageSize: 4 });
+
+  const [currrent, setCurrrent] = useState<any>({
+    id: productModel.current?.accessId,
+    name: productModel.current?.accessName,
+    protocol: productModel.current?.messageProtocol,
+    transport: productModel.current?.transportProtocol,
+    protocolDetail: {
+      name: productModel.current?.protocolName,
+    },
+  });
+
+  const handleSearch = (params: any) => {
+    setParam(params);
+    service
+      .queryList({ ...params, sorts: [{ name: 'createTime', order: 'desc' }] })
+      .then((resp) => {
+        setDataSource(resp.result);
+      });
+  };
+
+  const columns: ProColumns<any>[] = [
+    {
+      title: '名称',
+      dataIndex: 'name',
+    },
+  ];
+
+  useEffect(() => {
+    handleSearch(param);
+  }, []);
+
+  return (
+    <Modal
+      onCancel={() => {
+        close();
+      }}
+      visible
+      width={1200}
+      title={'设备接入配置'}
+      onOk={() => {
+        if (!!currrent) {
+          service1
+            .update({
+              ...productModel.current,
+              transportProtocol: currrent.transport,
+              protocolName: currrent.protocolDetail.name,
+              accessId: currrent.id,
+              accessName: currrent.name,
+              messageProtocol: currrent.protocol,
+            })
+            .then((resp) => {
+              if (resp.status === 200) {
+                service1.detail(productModel.current?.id || '').then((res) => {
+                  if (res.status === 200) {
+                    productModel.current = { ...res.result };
+                    message.success('操作成功!');
+                    close();
+                  }
+                });
+              }
+            });
+        } else {
+          message.success('请选择接入方式');
+        }
+      }}
+    >
+      <SearchComponent
+        field={columns}
+        pattern={'simple'}
+        onSearch={(data: any) => {
+          const dt = {
+            pageSize: 4,
+            terms: [...data.terms],
+          };
+          handleSearch(dt);
+        }}
+        onReset={() => {
+          handleSearch({ pageSize: 4 });
+        }}
+      />
+      <div style={{ display: 'flex', justifyContent: 'flex-end' }}>
+        <Button
+          type="primary"
+          onClick={() => {
+            const tab: any = window.open(`${origin}/#/link/AccessConfig/Detail`);
+            tab!.onTabSaveSuccess = (value: any) => {
+              if (value.status === 200) {
+                handleSearch(param);
+              }
+            };
+          }}
+        >
+          新增
+        </Button>
+      </div>
+      <Row gutter={[16, 16]} style={{ marginTop: 10 }}>
+        {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);
+              }}
+            >
+              <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.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>
+                  <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>
+                    <div className={styles.procotol}>
+                      <div className={styles.title}>{item?.protocolDetail?.name}</div>
+                      <p style={{ color: 'rgba(0, 0, 0, .55)' }}>{item.description}</p>
+                    </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={[4, 8, 16, 32]}
+          pageSize={dataSource?.pageSize}
+          showTotal={(num) => {
+            const minSize = dataSource?.pageIndex * dataSource?.pageSize + 1;
+            const MaxSize = (dataSource?.pageIndex + 1) * dataSource?.pageSize;
+            return `第 ${minSize} - ${MaxSize > num ? num : MaxSize} 条/总共 ${num} 条`;
+          }}
+        />
+      </div>
+    </Modal>
+  );
+};
+export default AccessConfig;

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

@@ -75,7 +75,7 @@
 }
 }
 
 
 .config {
 .config {
-  width: 48%;
+  width: 100%;
 
 
   .title {
   .title {
     width: 100%;
     width: 100%;

+ 34 - 198
src/pages/device/Product/Detail/Access/index.tsx

@@ -1,37 +1,11 @@
-import {
-  Alert,
-  Badge,
-  Button,
-  Card,
-  Col,
-  Descriptions,
-  message,
-  Pagination,
-  Row,
-  Table,
-  Tooltip,
-} from 'antd';
+import { Badge, Descriptions, Empty, Table, Tooltip } from 'antd';
 import { service } from '@/pages/link/AccessConfig';
 import { service } from '@/pages/link/AccessConfig';
 import styles from './index.less';
 import styles from './index.less';
 import { useEffect, useState } from 'react';
 import { useEffect, useState } from 'react';
-import Service from '@/pages/device/Product/service';
 import { productModel } from '@/pages/device/Product';
 import { productModel } from '@/pages/device/Product';
-import type { ProColumns } from '@jetlinks/pro-table';
-import SearchComponent from '@/components/SearchComponent';
-import { getMenuPathByCode, MENUS_CODE } from '@/utils/menu';
-import { useHistory } from 'umi';
+import AccessConfig from './AccessConfig';
 
 
 const Access = () => {
 const Access = () => {
-  const service1 = new Service('device-product');
-  const [currrent, setCurrrent] = useState<any>({
-    id: productModel.current?.accessId,
-    name: productModel.current?.accessName,
-    protocol: productModel.current?.messageProtocol,
-    transport: productModel.current?.transportProtocol,
-    protocolDetail: {
-      name: productModel.current?.protocolName,
-    },
-  });
   const [visible, setVisible] = useState<boolean>(true);
   const [visible, setVisible] = useState<boolean>(true);
   const [config, setConfig] = useState<any>();
   const [config, setConfig] = useState<any>();
   const [access, setAccess] = useState<any>();
   const [access, setAccess] = useState<any>();
@@ -47,15 +21,7 @@ const Access = () => {
   MetworkTypeMapping.set('mqtt-server-gateway', 'MQTT_SERVER');
   MetworkTypeMapping.set('mqtt-server-gateway', 'MQTT_SERVER');
   MetworkTypeMapping.set('tcp-server-gateway', 'TCP_SERVER');
   MetworkTypeMapping.set('tcp-server-gateway', 'TCP_SERVER');
 
 
-  const [param, setParam] = useState<any>({ pageSize: 10 });
-  const history = useHistory();
-
-  const columns: ProColumns<any>[] = [
-    {
-      title: '名称',
-      dataIndex: 'name',
-    },
-  ];
+  const [configVisible, setConfigVisible] = useState<boolean>(false);
 
 
   const queryNetworkList = (id: string) => {
   const queryNetworkList = (id: string) => {
     service.getNetworkList(MetworkTypeMapping.get(id)).then((resp) => {
     service.getNetworkList(MetworkTypeMapping.get(id)).then((resp) => {
@@ -65,22 +31,6 @@ const Access = () => {
     });
     });
   };
   };
 
 
-  const [dataSource, setDataSource] = useState<any>({
-    data: [],
-    pageSize: 10,
-    pageIndex: 0,
-    total: 0,
-  });
-
-  const handleSearch = (params: any) => {
-    setParam(params);
-    service
-      .queryList({ ...params, sorts: [{ name: 'createTime', order: 'desc' }] })
-      .then((resp) => {
-        setDataSource(resp.result);
-      });
-  };
-
   const queryProviders = () => {
   const queryProviders = () => {
     service.getProviders().then((resp) => {
     service.getProviders().then((resp) => {
       if (resp.status === 200) {
       if (resp.status === 200) {
@@ -224,151 +174,42 @@ const Access = () => {
         productModel.current?.transportProtocol || '',
         productModel.current?.transportProtocol || '',
       );
       );
       queryAccess(productModel.current?.accessId);
       queryAccess(productModel.current?.accessId);
-    } else {
-      handleSearch(param);
     }
     }
-  }, []);
+  }, [productModel.current]);
 
 
   return (
   return (
     <div>
     <div>
       {!visible ? (
       {!visible ? (
-        <div style={{ padding: '20px 0' }}>
-          <Alert message="请选择使用的设备接入网关,用以提供设备接入能力" type="info" showIcon />
-          <SearchComponent
-            field={columns}
-            pattern={'simple'}
-            onSearch={(data: any) => {
-              const dt = {
-                pageSize: 10,
-                terms: [...data.terms],
-              };
-              handleSearch(dt);
-            }}
-            onReset={() => {
-              handleSearch({ pageSize: 10 });
-            }}
-          />
-          <Button
-            type="primary"
-            onClick={() => {
-              history.push(`${getMenuPathByCode(MENUS_CODE['link/AccessConfig/Detail'])}`);
-            }}
-          >
-            新增
-          </Button>
-          <Row gutter={[16, 16]} style={{ marginTop: 10 }}>
-            {dataSource.data.map((item: any) => (
-              <Col key={item.name} span={12}>
-                <Card
-                  style={{
-                    width: '100%',
-                    border: currrent?.id === item.id ? '1px solid #1d39c4' : '',
-                  }}
-                  hoverable
+        <div style={{ padding: '100px 0' }}>
+          <Empty
+            description={
+              <span>
+                请先
+                <a
                   onClick={() => {
                   onClick={() => {
-                    setCurrrent(item);
+                    setConfigVisible(true);
                   }}
                   }}
                 >
                 >
-                  <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.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>
-                      <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>
-                        <div className={styles.procotol}>
-                          <div className={styles.title}>{item?.protocolDetail?.name}</div>
-                          <p style={{ color: 'rgba(0, 0, 0, .55)' }}>{item.description}</p>
-                        </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>
-          <div style={{ marginTop: '20px' }}>
-            <Button
-              type="primary"
+                  选择
+                </a>
+                设备接入网关,用以提供设备接入能力
+              </span>
+            }
+          />
+        </div>
+      ) : (
+        <div className={styles.config}>
+          <div className={styles.title}>
+            配置概览
+            <a
+              style={{ marginLeft: 20 }}
               onClick={() => {
               onClick={() => {
-                if (!!currrent) {
-                  service1
-                    .update({
-                      ...productModel.current,
-                      transportProtocol: currrent.transport,
-                      protocolName: currrent.protocolDetail.name,
-                      accessId: currrent.id,
-                      accessName: currrent.name,
-                      messageProtocol: currrent.protocol,
-                    })
-                    .then((resp) => {
-                      if (resp.status === 200) {
-                        service1.detail(productModel.current?.id || '').then((res) => {
-                          if (res.status === 200) {
-                            productModel.current = { ...res.result };
-                            message.success('操作成功!');
-                            setVisible(true);
-                            getDetail(res.result.messageProtocol, res.result.transportProtocol);
-                            queryAccess(res.result.accessId);
-                          }
-                        });
-                      }
-                    });
-                } else {
-                  message.success('请选择接入方式');
-                }
+                setConfigVisible(true);
               }}
               }}
             >
             >
-              保存
-            </Button>
+              更换
+            </a>
           </div>
           </div>
-        </div>
-      ) : (
-        <div className={styles.config}>
-          <div className={styles.title}>配置概览</div>
           <Descriptions column={1}>
           <Descriptions column={1}>
             <Descriptions.Item label="接入方式">
             <Descriptions.Item label="接入方式">
               {providers.find((i) => i.id === access?.provider)?.name || ''}
               {providers.find((i) => i.id === access?.provider)?.name || ''}
@@ -405,20 +246,15 @@ const Access = () => {
               />
               />
             </div>
             </div>
           )}
           )}
-
-          <div style={{ marginTop: '20px' }}>
-            <Button
-              type="primary"
-              onClick={() => {
-                setVisible(false);
-                handleSearch({ pageSize: 10 });
-              }}
-            >
-              编辑
-            </Button>
-          </div>
         </div>
         </div>
       )}
       )}
+      {configVisible && (
+        <AccessConfig
+          close={() => {
+            setConfigVisible(false);
+          }}
+        />
+      )}
     </div>
     </div>
   );
   );
 };
 };

+ 141 - 116
src/pages/device/Product/Detail/BaseInfo/index.tsx

@@ -1,129 +1,152 @@
 import { productModel, service } from '@/pages/device/Product';
 import { productModel, service } from '@/pages/device/Product';
-import { Button, Descriptions } from 'antd';
-import { useState } from 'react';
-import { useParams } from 'umi';
+import { Button, Card, Descriptions, Divider, message } from 'antd';
+import type { SetStateAction } from 'react';
+import { useEffect, useState } from 'react';
 import { useIntl } from '@@/plugin-locale/localeExports';
 import { useIntl } from '@@/plugin-locale/localeExports';
 import { EditOutlined } from '@ant-design/icons';
 import { EditOutlined } from '@ant-design/icons';
 import { getDateFormat } from '@/utils/util';
 import { getDateFormat } from '@/utils/util';
 import Save from '@/pages/device/Product/Save';
 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 BaseInfo = () => {
   const intl = useIntl();
   const intl = useIntl();
-  const param = useParams<{ id: string }>();
-  // 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 [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 (param.id) {
-  //     service
-  //       .getConfigMetadata(param.id)
-  //       .then((config: { result: SetStateAction<ConfigMetadata[]> }) => {
-  //         setMetadata(config.result);
-  //       });
-  //   }
-  // }, [param.id]);
+  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 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 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 getDetailInfo = () => {
   const getDetailInfo = () => {
-    service.getProductDetail(param?.id).subscribe((data) => {
+    service.getProductDetail(id || '').subscribe((data) => {
       if (data) {
       if (data) {
         productModel.current = data;
         productModel.current = data;
       }
       }
     });
     });
   };
   };
 
 
-  // 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 (
-  //         <Card
-  //           key={item.name}
-  //           title={item.name}
-  //           extra={
-  //             <a onClick={() => 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>
-  //       );
-  //     })
-  //   ) : (
-  //     <Empty description={'暂无配置'} />
-  //   );
-  // };
+  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 (
   return (
     <>
     <>
@@ -162,35 +185,36 @@ const BaseInfo = () => {
         </Descriptions.Item>
         </Descriptions.Item>
         <Descriptions.Item
         <Descriptions.Item
           label={intl.formatMessage({
           label={intl.formatMessage({
-            id: 'pages.device.productDetail.protocolName',
-            defaultMessage: '消息协议',
+            id: 'pages.device.instanceDetail.deviceType',
+            defaultMessage: '设备类型',
           })}
           })}
         >
         >
-          {productModel.current?.protocolName}
+          {productModel.current?.deviceType ? productModel.current?.deviceType.text : '-'}
         </Descriptions.Item>
         </Descriptions.Item>
         <Descriptions.Item
         <Descriptions.Item
           label={intl.formatMessage({
           label={intl.formatMessage({
             id: 'pages.device.productDetail.transportProtocol',
             id: 'pages.device.productDetail.transportProtocol',
-            defaultMessage: '链接协议',
+            defaultMessage: '接入方式',
           })}
           })}
         >
         >
           {productModel.current?.transportProtocol}
           {productModel.current?.transportProtocol}
         </Descriptions.Item>
         </Descriptions.Item>
+
         <Descriptions.Item
         <Descriptions.Item
           label={intl.formatMessage({
           label={intl.formatMessage({
-            id: 'pages.device.productDetail.updateTime',
-            defaultMessage: '更新时间',
+            id: 'pages.device.productDetail.createTime',
+            defaultMessage: '创建时间',
           })}
           })}
         >
         >
-          {getDateFormat(productModel.current?.updateTime)}
+          {getDateFormat(productModel.current?.createTime)}
         </Descriptions.Item>
         </Descriptions.Item>
         <Descriptions.Item
         <Descriptions.Item
           label={intl.formatMessage({
           label={intl.formatMessage({
-            id: 'pages.device.productDetail.createTime',
-            defaultMessage: '创建时间',
+            id: 'pages.device.productDetail.updateTime',
+            defaultMessage: '更新时间',
           })}
           })}
         >
         >
-          {getDateFormat(productModel.current?.createTime)}
+          {getDateFormat(productModel.current?.updateTime)}
         </Descriptions.Item>
         </Descriptions.Item>
         <Descriptions.Item
         <Descriptions.Item
           span={3}
           span={3}
@@ -211,6 +235,7 @@ const BaseInfo = () => {
         reload={getDetailInfo}
         reload={getDetailInfo}
         visible={visible}
         visible={visible}
       />
       />
+      {renderConfigCard()}
     </>
     </>
   );
   );
 };
 };

+ 12 - 22
src/pages/device/Product/Detail/index.tsx

@@ -1,17 +1,6 @@
 import { PageContainer } from '@ant-design/pro-layout';
 import { PageContainer } from '@ant-design/pro-layout';
 import { history, Link, useParams } from 'umi';
 import { history, Link, useParams } from 'umi';
-import {
-  Badge,
-  Button,
-  Card,
-  Descriptions,
-  message,
-  Space,
-  Spin,
-  Switch,
-  Tabs,
-  Tooltip,
-} from 'antd';
+import { Badge, Card, Descriptions, message, Space, Spin, Switch, Tabs, Tooltip } from 'antd';
 import BaseInfo from '@/pages/device/Product/Detail/BaseInfo';
 import BaseInfo from '@/pages/device/Product/Detail/BaseInfo';
 import { observer } from '@formily/react';
 import { observer } from '@formily/react';
 import { productModel, service } from '@/pages/device/Product';
 import { productModel, service } from '@/pages/device/Product';
@@ -152,21 +141,22 @@ const ProductDetail = observer(() => {
       subTitle={
       subTitle={
         <Switch
         <Switch
           key={2}
           key={2}
-          checkedChildren="启用"
-          unCheckedChildren="停用"
+          checked={productModel.current?.state === 1}
+          checkedChildren="已发布"
+          unCheckedChildren="未发布"
           onChange={() => {
           onChange={() => {
             changeDeploy(statusMap[productModel.current?.state || 0].action);
             changeDeploy(statusMap[productModel.current?.state || 0].action);
           }}
           }}
         />
         />
       }
       }
-      extra={[
-        <Button key="1" type="primary" onClick={() => changeDeploy('deploy')}>
-          {intl.formatMessage({
-            id: 'pages.device.productDetail.setting',
-            defaultMessage: '应用配置',
-          })}
-        </Button>,
-      ]}
+      // extra={[
+      //   <Button key="1" type="primary" onClick={() => changeDeploy('deploy')}>
+      //     {intl.formatMessage({
+      //       id: 'pages.device.productDetail.setting',
+      //       defaultMessage: '应用配置',
+      //     })}
+      //   </Button>,
+      // ]}
     >
     >
       <Card>
       <Card>
         <Tabs defaultActiveKey="base">
         <Tabs defaultActiveKey="base">

+ 6 - 3
src/pages/device/Product/Save/index.tsx

@@ -30,7 +30,7 @@ const Save = (props: Props) => {
     // 特殊处理deviceType字段
     // 特殊处理deviceType字段
     if (data) {
     if (data) {
       if (typeof data.deviceType !== 'string') {
       if (typeof data.deviceType !== 'string') {
-        data.deviceType = data.deviceType?.value;
+        data.deviceTypeId = data.deviceType?.value;
       }
       }
     }
     }
     return data;
     return data;
@@ -71,13 +71,16 @@ const Save = (props: Props) => {
   const handleSave = async () => {
   const handleSave = async () => {
     const formData = await form.validateFields();
     const formData = await form.validateFields();
     if (formData) {
     if (formData) {
-      const res = await service.update(formData);
+      const { deviceTypeId, ...extraFormData } = formData;
+      extraFormData.deviceType = formData.deviceTypeId;
+      const res = await service.update(extraFormData);
       if (res.status === 200) {
       if (res.status === 200) {
         message.success('保存成功');
         message.success('保存成功');
         if (props.reload) {
         if (props.reload) {
           props.reload();
           props.reload();
         }
         }
         props.close();
         props.close();
+        form.resetFields();
       }
       }
     }
     }
   };
   };
@@ -215,7 +218,7 @@ const Save = (props: Props) => {
           <Col span={24}>
           <Col span={24}>
             <Form.Item
             <Form.Item
               label={intlFormat('pages.device.instanceDetail.deviceType', '设备类型')}
               label={intlFormat('pages.device.instanceDetail.deviceType', '设备类型')}
-              name={'deviceType'}
+              name={'deviceTypeId'}
               rules={[
               rules={[
                 {
                 {
                   required: true,
                   required: true,

+ 120 - 103
src/pages/device/Product/index.tsx

@@ -16,12 +16,12 @@ import { model } from '@formily/reactive';
 import { Link, useHistory } from 'umi';
 import { Link, useHistory } from 'umi';
 import { useIntl } from '@@/plugin-locale/localeExports';
 import { useIntl } from '@@/plugin-locale/localeExports';
 import type { ActionType, ProColumns } from '@jetlinks/pro-table';
 import type { ActionType, ProColumns } from '@jetlinks/pro-table';
-import ProTable from '@jetlinks/pro-table';
 import { useEffect, useRef, useState } from 'react';
 import { useEffect, useRef, useState } from 'react';
-import encodeQuery from '@/utils/encodeQuery';
 import Save from '@/pages/device/Product/Save';
 import Save from '@/pages/device/Product/Save';
 import SearchComponent from '@/components/SearchComponent';
 import SearchComponent from '@/components/SearchComponent';
 import { getMenuPathByParams, MENUS_CODE } from '@/utils/menu';
 import { getMenuPathByParams, MENUS_CODE } from '@/utils/menu';
+import { ProTableCard } from '@/components';
+import ProductCard from '@/components/ProTableCard/CardItems/product';
 
 
 export const service = new Service('device-product');
 export const service = new Service('device-product');
 export const statusMap = {
 export const statusMap = {
@@ -105,6 +105,109 @@ const Product = observer(() => {
     });
     });
   };
   };
 
 
+  const tools = (record: ProductItem) => [
+    <Tooltip
+      title={intl.formatMessage({
+        id: 'pages.data.option.detail',
+        defaultMessage: '查看',
+      })}
+      key={'detail'}
+    >
+      <Link
+        onClick={() => {
+          productModel.current = record;
+        }}
+        to={`${getMenuPathByParams(MENUS_CODE['device/Product/Detail'], record.id)}`}
+        key="link"
+      >
+        <EyeOutlined />
+      </Link>
+    </Tooltip>,
+    <Tooltip
+      title={intl.formatMessage({
+        id: 'pages.data.option.edit',
+        defaultMessage: '编辑',
+      })}
+      key={'edit'}
+    >
+      <Button
+        key="warning"
+        onClick={() => {
+          setCurrent(record);
+          setVisible(true);
+        }}
+        type={'link'}
+      >
+        <EditOutlined />
+      </Button>
+    </Tooltip>,
+    <Tooltip
+      title={intl.formatMessage({
+        id: 'pages.data.option.download',
+        defaultMessage: '下载',
+      })}
+      key={'download'}
+    >
+      <Button type={'link'}>
+        <DownloadOutlined
+          onClick={async () => {
+            await message.success(
+              `${intl.formatMessage({
+                id: 'pages.data.option.download',
+                defaultMessage: '下载',
+              })}`,
+            );
+          }}
+        />
+      </Button>
+    </Tooltip>,
+    <Popconfirm
+      key={'state'}
+      title={intl.formatMessage({
+        id: `pages.data.option.${record.state ? 'disabled' : 'enabled'}.tips`,
+        defaultMessage: '是否启用?',
+      })}
+      onConfirm={() => {
+        changeDeploy(record.id, record.state ? 'undeploy' : 'deploy');
+      }}
+    >
+      <Tooltip
+        title={intl.formatMessage({
+          id: `pages.data.option.${record.state ? 'disabled' : 'enabled'}`,
+          defaultMessage: record.state ? '禁用' : '启用',
+        })}
+      >
+        <Button type={'link'}>{record.state ? <StopOutlined /> : <PlayCircleOutlined />}</Button>
+      </Tooltip>
+    </Popconfirm>,
+    <Popconfirm
+      key="unBindUser"
+      title={intl.formatMessage({
+        id: record.state === 1 ? 'pages.device.productDetail.deleteTip' : 'page.table.isDelete',
+        defaultMessage: '是否删除?',
+      })}
+      onConfirm={async () => {
+        if (record.state === 0) {
+          await deleteItem(record.id);
+        } else {
+          message.error('已发布的产品不能进行删除操作');
+        }
+      }}
+    >
+      <Tooltip
+        title={intl.formatMessage({
+          id: 'pages.data.option.remove',
+          defaultMessage: '删除',
+        })}
+        key={'remove'}
+      >
+        <a key="delete">
+          <DeleteOutlined />
+        </a>
+      </Tooltip>
+    </Popconfirm>,
+  ];
+
   const columns: ProColumns<ProductItem>[] = [
   const columns: ProColumns<ProductItem>[] = [
     {
     {
       title: 'ID',
       title: 'ID',
@@ -116,7 +219,8 @@ const Product = observer(() => {
     },
     },
     {
     {
       title: '设备类型',
       title: '设备类型',
-      dataIndex: 'classifiedName',
+      dataIndex: 'deviceType',
+      render: (_, row) => <>{row.deviceType ? row.deviceType.text : undefined}</>,
     },
     },
     {
     {
       title: '状态',
       title: '状态',
@@ -130,110 +234,14 @@ const Product = observer(() => {
       valueType: 'option',
       valueType: 'option',
       align: 'center',
       align: 'center',
       width: 200,
       width: 200,
-      render: (_, record) => [
-        <Tooltip
-          title={intl.formatMessage({
-            id: 'pages.data.option.detail',
-            defaultMessage: '查看',
-          })}
-          key={'detail'}
-        >
-          <Link
-            onClick={() => {
-              productModel.current = record;
-            }}
-            to={`${getMenuPathByParams(MENUS_CODE['device/Product/Detail'], record.id)}`}
-            key="link"
-          >
-            <EyeOutlined />
-          </Link>
-        </Tooltip>,
-        <Tooltip
-          title={intl.formatMessage({
-            id: 'pages.data.option.edit',
-            defaultMessage: '编辑',
-          })}
-          key={'edit'}
-        >
-          <a
-            key="warning"
-            onClick={() => {
-              setCurrent(record);
-              setVisible(true);
-            }}
-          >
-            <EditOutlined />
-          </a>
-        </Tooltip>,
-        <Tooltip
-          title={intl.formatMessage({
-            id: 'pages.data.option.download',
-            defaultMessage: '下载',
-          })}
-          key={'download'}
-        >
-          <a key="download">
-            <DownloadOutlined
-              onClick={async () => {
-                await message.success(
-                  `${intl.formatMessage({
-                    id: 'pages.data.option.download',
-                    defaultMessage: '下载',
-                  })}`,
-                );
-              }}
-            />
-          </a>
-        </Tooltip>,
-        <Popconfirm
-          key={'state'}
-          title={intl.formatMessage({
-            id: `pages.data.option.${record.state ? 'disabled' : 'enabled'}.tips`,
-            defaultMessage: '是否删除?',
-          })}
-          onConfirm={() => {
-            changeDeploy(record.id, record.state ? 'undeploy' : 'deploy');
-          }}
-        >
-          <Tooltip
-            title={intl.formatMessage({
-              id: `pages.data.option.${record.state ? 'disabled' : 'enabled'}`,
-              defaultMessage: record.state ? '禁用' : '启用',
-            })}
-          >
-            <a key="state">{record.state ? <StopOutlined /> : <PlayCircleOutlined />}</a>
-          </Tooltip>
-        </Popconfirm>,
-        <Popconfirm
-          key="unBindUser"
-          title={intl.formatMessage({
-            id: 'page.system.menu.table.delete',
-            defaultMessage: '是否删除?',
-          })}
-          onConfirm={async () => {
-            await deleteItem(record.id);
-          }}
-        >
-          <Tooltip
-            title={intl.formatMessage({
-              id: 'pages.data.option.remove.tips',
-              defaultMessage: '删除',
-            })}
-            key={'remove'}
-          >
-            <a key="delete">
-              <DeleteOutlined />
-            </a>
-          </Tooltip>
-        </Popconfirm>,
-      ],
+      render: (_, record) => tools(record),
     },
     },
   ];
   ];
 
 
   return (
   return (
     <PageContainer>
     <PageContainer>
       <SearchComponent field={columns} onSearch={searchFn} />
       <SearchComponent field={columns} onSearch={searchFn} />
-      <ProTable<ProductItem>
+      <ProTableCard<ProductItem>
         columns={columns}
         columns={columns}
         actionRef={actionRef}
         actionRef={actionRef}
         options={{ fullScreen: true }}
         options={{ fullScreen: true }}
@@ -244,12 +252,20 @@ const Product = observer(() => {
         // }}
         // }}
         params={queryParam}
         params={queryParam}
         request={(params = {}) =>
         request={(params = {}) =>
-          service.query(encodeQuery({ ...params, sorts: { createTime: 'ascend' } }))
+          service.query({
+            ...params,
+            sorts: [
+              {
+                name: 'createTime',
+                order: 'desc',
+              },
+            ],
+          })
         }
         }
         rowKey="id"
         rowKey="id"
         search={false}
         search={false}
         pagination={{ pageSize: 10 }}
         pagination={{ pageSize: 10 }}
-        toolBarRender={() => [
+        headerTitle={[
           <Button
           <Button
             onClick={() => {
             onClick={() => {
               setCurrent(undefined);
               setCurrent(undefined);
@@ -265,6 +281,7 @@ const Product = observer(() => {
             })}
             })}
           </Button>,
           </Button>,
         ]}
         ]}
+        cardRender={(record) => <ProductCard {...record} actions={tools(record)} />}
       />
       />
       <Save
       <Save
         model={!current ? 'add' : 'edit'}
         model={!current ? 'add' : 'edit'}

+ 3 - 1
src/pages/device/Product/typings.d.ts

@@ -14,7 +14,8 @@ export type ProductItem = {
   createTime: number;
   createTime: number;
   updateTime: number;
   updateTime: number;
   creatorId: string;
   creatorId: string;
-  deviceType: string | DeviceType;
+  deviceType: DeviceType;
+  deviceTypeId?: string;
   count?: number;
   count?: number;
   messageProtocol: string;
   messageProtocol: string;
   metadata: string;
   metadata: string;
@@ -25,6 +26,7 @@ export type ProductItem = {
   describe?: string;
   describe?: string;
   accessId?: string;
   accessId?: string;
   accessName?: string;
   accessName?: string;
+  photoUrl?: string;
 };
 };
 
 
 export type ConfigProperty = {
 export type ConfigProperty = {

+ 93 - 58
src/pages/link/AccessConfig/Detail/Access/index.tsx

@@ -18,14 +18,17 @@ import { useEffect, useState } from 'react';
 import styles from './index.less';
 import styles from './index.less';
 import { service } from '@/pages/link/AccessConfig';
 import { service } from '@/pages/link/AccessConfig';
 import encodeQuery from '@/utils/encodeQuery';
 import encodeQuery from '@/utils/encodeQuery';
-import { useHistory } from 'umi';
-import { getMenuPathByCode, MENUS_CODE } from '@/utils/menu';
+import { useHistory, useLocation } from 'umi';
 
 
 interface Props {
 interface Props {
   change: () => void;
   change: () => void;
   data: any;
   data: any;
-  access: any;
 }
 }
+
+type LocationType = {
+  id?: string;
+};
+
 const Access = (props: Props) => {
 const Access = (props: Props) => {
   const [form] = Form.useForm();
   const [form] = Form.useForm();
 
 
@@ -34,8 +37,9 @@ const Access = (props: Props) => {
   const [current, setCurrent] = useState<number>(0);
   const [current, setCurrent] = useState<number>(0);
   const [networkList, setNetworkList] = useState<any[]>([]);
   const [networkList, setNetworkList] = useState<any[]>([]);
   const [procotolList, setProcotolList] = useState<any[]>([]);
   const [procotolList, setProcotolList] = useState<any[]>([]);
-  const [procotolCurrent, setProcotolCurrent] = useState<string>(props.access?.protocol || '');
-  const [networkCurrent, setNetworkCurrent] = useState<string>(props.access?.channelId || '');
+  const [access, setAccess] = useState<any>({});
+  const [procotolCurrent, setProcotolCurrent] = useState<string>('');
+  const [networkCurrent, setNetworkCurrent] = useState<string>('');
   const [config, setConfig] = useState<any>();
   const [config, setConfig] = useState<any>();
   const [providers, setProviders] = useState<any[]>([]);
   const [providers, setProviders] = useState<any[]>([]);
 
 
@@ -57,24 +61,20 @@ const Access = (props: Props) => {
   ProcotoleMapping.set('mqtt-server-gateway', 'MQTT');
   ProcotoleMapping.set('mqtt-server-gateway', 'MQTT');
   ProcotoleMapping.set('tcp-server-gateway', 'TCP');
   ProcotoleMapping.set('tcp-server-gateway', 'TCP');
 
 
-  const queryNetworkList = (params?: any) => {
-    service
-      .getNetworkList(MetworkTypeMapping.get(props.data?.id || props.access?.provider), params)
-      .then((resp) => {
-        if (resp.status === 200) {
-          setNetworkList(resp.result);
-        }
-      });
+  const queryNetworkList = (id: string, params?: any) => {
+    service.getNetworkList(MetworkTypeMapping.get(id), params).then((resp) => {
+      if (resp.status === 200) {
+        setNetworkList(resp.result);
+      }
+    });
   };
   };
 
 
-  const queryProcotolList = (params?: any) => {
-    service
-      .getProtocolList(ProcotoleMapping.get(props.data?.id || props.access?.provider), params)
-      .then((resp) => {
-        if (resp.status === 200) {
-          setProcotolList(resp.result);
-        }
-      });
+  const queryProcotolList = (id: string, params?: any) => {
+    service.getProtocolList(ProcotoleMapping.get(id), params).then((resp) => {
+      if (resp.status === 200) {
+        setProcotolList(resp.result);
+      }
+    });
   };
   };
 
 
   const queryProviders = () => {
   const queryProviders = () => {
@@ -86,26 +86,39 @@ const Access = (props: Props) => {
   };
   };
 
 
   useEffect(() => {
   useEffect(() => {
-    if (props.data) {
-      queryNetworkList();
+    if (props.data?.id) {
+      queryNetworkList(props.data?.id);
       setCurrent(0);
       setCurrent(0);
     }
     }
   }, [props.data]);
   }, [props.data]);
 
 
+  const location = useLocation<LocationType>();
+
+  const params = new URLSearchParams(location.search);
+
   useEffect(() => {
   useEffect(() => {
-    form.setFieldsValue({
-      name: props.access?.name,
-      description: props.access?.name,
-    });
-    queryProviders();
-  }, [props.access]);
+    if (params.get('id')) {
+      service.detail(params.get('id') || '').then((resp) => {
+        setAccess(resp.result);
+        setProcotolCurrent(resp.result?.protocol);
+        setNetworkCurrent(resp.result?.channelId);
+        form.setFieldsValue({
+          name: resp.result?.name,
+          description: resp.result?.description,
+        });
+        queryProviders();
+        setCurrent(0);
+        queryNetworkList(resp.result?.provider);
+      });
+    }
+  }, []);
 
 
   const next = () => {
   const next = () => {
     if (current === 0) {
     if (current === 0) {
       if (!networkCurrent) {
       if (!networkCurrent) {
         message.error('请选择网络组件!');
         message.error('请选择网络组件!');
       } else {
       } else {
-        queryProcotolList();
+        queryProcotolList(props.data?.id || access?.provider);
         setCurrent(current + 1);
         setCurrent(current + 1);
       }
       }
     }
     }
@@ -114,10 +127,7 @@ const Access = (props: Props) => {
         message.error('请选择消息协议!');
         message.error('请选择消息协议!');
       } else {
       } else {
         service
         service
-          .getConfigView(
-            procotolCurrent,
-            ProcotoleMapping.get(props.data?.id || props.access?.provider),
-          )
+          .getConfigView(procotolCurrent, ProcotoleMapping.get(props.data?.id || access?.provider))
           .then((resp) => {
           .then((resp) => {
             if (resp.status === 200) {
             if (resp.status === 200) {
               setConfig(resp.result);
               setConfig(resp.result);
@@ -264,6 +274,7 @@ const Access = (props: Props) => {
                 placeholder="请输入名称"
                 placeholder="请输入名称"
                 onSearch={(value: string) => {
                 onSearch={(value: string) => {
                   queryNetworkList(
                   queryNetworkList(
+                    props.data?.id || access?.provider,
                     encodeQuery({
                     encodeQuery({
                       terms: {
                       terms: {
                         name$LIKE: `%${value}%`,
                         name$LIKE: `%${value}%`,
@@ -276,7 +287,12 @@ const Access = (props: Props) => {
               <Button
               <Button
                 type="primary"
                 type="primary"
                 onClick={() => {
                 onClick={() => {
-                  history.push(`${getMenuPathByCode(MENUS_CODE['link/Type/Save'])}`);
+                  const tab: any = window.open(`${origin}/#/link/Type/Save/:id`);
+                  tab!.onTabSaveSuccess = (value: any) => {
+                    if (value.status === 200) {
+                      queryNetworkList(props.data?.id || access?.provider);
+                    }
+                  };
                 }}
                 }}
               >
               >
                 新增
                 新增
@@ -312,16 +328,17 @@ const Access = (props: Props) => {
               </Row>
               </Row>
             ) : (
             ) : (
               <Empty
               <Empty
-                image="https://gw.alipayobjects.com/zos/antfincdn/ZHrcdLPrvN/empty.svg"
-                imageStyle={{
-                  height: 60,
-                }}
                 description={
                 description={
                   <span>
                   <span>
                     暂无数据{' '}
                     暂无数据{' '}
                     <a
                     <a
                       onClick={() => {
                       onClick={() => {
-                        history.push(`${getMenuPathByCode(MENUS_CODE['link/Type/Save'])}`);
+                        const tab: any = window.open(`${origin}/#/link/Type/Save/:id`);
+                        tab!.onTabSaveSuccess = (value: any) => {
+                          if (value.status === 200) {
+                            queryNetworkList(props.data?.id || access?.provider);
+                          }
+                        };
                       }}
                       }}
                     >
                     >
                       创建接入方式
                       创建接入方式
@@ -345,6 +362,7 @@ const Access = (props: Props) => {
                 placeholder="请输入名称"
                 placeholder="请输入名称"
                 onSearch={(value: string) => {
                 onSearch={(value: string) => {
                   queryProcotolList(
                   queryProcotolList(
+                    props.data?.id || access?.provider,
                     encodeQuery({
                     encodeQuery({
                       terms: {
                       terms: {
                         name$LIKE: `%${value}%`,
                         name$LIKE: `%${value}%`,
@@ -357,7 +375,12 @@ const Access = (props: Props) => {
               <Button
               <Button
                 type="primary"
                 type="primary"
                 onClick={() => {
                 onClick={() => {
-                  history.push(`${getMenuPathByCode(MENUS_CODE['link/Protocol'])}`);
+                  const tab: any = window.open(`${origin}/#/link/Protocol?save=true`);
+                  tab!.onTabSaveSuccess = (value: any) => {
+                    if (value) {
+                      queryProcotolList(props.data?.id || access?.provider);
+                    }
+                  };
                 }}
                 }}
               >
               >
                 新增
                 新增
@@ -385,16 +408,17 @@ const Access = (props: Props) => {
               </Row>
               </Row>
             ) : (
             ) : (
               <Empty
               <Empty
-                image="https://gw.alipayobjects.com/zos/antfincdn/ZHrcdLPrvN/empty.svg"
-                imageStyle={{
-                  height: 60,
-                }}
                 description={
                 description={
                   <span>
                   <span>
                     暂无数据
                     暂无数据
                     <a
                     <a
                       onClick={() => {
                       onClick={() => {
-                        history.push(`${getMenuPathByCode(MENUS_CODE['link/Protocol'])}`);
+                        const tab: any = window.open(`${origin}/#/link/Protocol?save=true`);
+                        tab!.onTabSaveSuccess = (value: any) => {
+                          if (value) {
+                            queryProcotolList(props.data?.id || access?.provider);
+                          }
+                        };
                       }}
                       }}
                     >
                     >
                       去新增
                       去新增
@@ -427,9 +451,12 @@ const Access = (props: Props) => {
               <div className={styles.title}>配置概览</div>
               <div className={styles.title}>配置概览</div>
               <Descriptions column={1}>
               <Descriptions column={1}>
                 <Descriptions.Item label="接入方式">
                 <Descriptions.Item label="接入方式">
-                  {props.data?.name || providers.find((i) => i.id === props.access?.provider)?.name}
+                  {props.data?.name || providers.find((i) => i.id === access?.provider)?.name}
+                </Descriptions.Item>
+                <Descriptions.Item>
+                  {props.data?.description ||
+                    providers.find((i) => i.id === access?.provider)?.description}
                 </Descriptions.Item>
                 </Descriptions.Item>
-                <Descriptions.Item>{props.data?.description || ''}</Descriptions.Item>
                 <Descriptions.Item label="消息协议">
                 <Descriptions.Item label="消息协议">
                   {procotolList.find((i) => i.id === procotolCurrent)?.name || ''}
                   {procotolList.find((i) => i.id === procotolCurrent)?.name || ''}
                 </Descriptions.Item>
                 </Descriptions.Item>
@@ -508,8 +535,8 @@ const Access = (props: Props) => {
                 try {
                 try {
                   const values = await form.validateFields();
                   const values = await form.validateFields();
                   // 编辑还是保存
                   // 编辑还是保存
-                  if (!!props.access.id) {
-                    const params = {
+                  if (!params.get('id')) {
+                    const param = {
                       name: values.name,
                       name: values.name,
                       description: values.description,
                       description: values.description,
                       provider: props.data.id,
                       provider: props.data.id,
@@ -518,27 +545,35 @@ const Access = (props: Props) => {
                       channel: 'network', // 网络组件
                       channel: 'network', // 网络组件
                       channelId: networkCurrent,
                       channelId: networkCurrent,
                     };
                     };
-                    service.save(params).then((resp: any) => {
+                    service.save(param).then((resp: any) => {
                       if (resp.status === 200) {
                       if (resp.status === 200) {
                         message.success('操作成功!');
                         message.success('操作成功!');
-                        history.push(`${getMenuPathByCode(MENUS_CODE['link/AccessConfig'])}`);
+                        history.goBack();
+                        if ((window as any).onTabSaveSuccess) {
+                          (window as any).onTabSaveSuccess(resp);
+                          setTimeout(() => window.close(), 300);
+                        }
                       }
                       }
                     });
                     });
                   } else {
                   } else {
-                    const params = {
-                      id: props.access?.id,
+                    const param = {
+                      id: access?.id,
                       name: values.name,
                       name: values.name,
                       description: values.description,
                       description: values.description,
-                      provider: props.data.id,
+                      provider: access?.provider,
                       protocol: procotolCurrent,
                       protocol: procotolCurrent,
-                      transport: ProcotoleMapping.get(props.data.id),
+                      transport: access?.transport,
                       channel: 'network', // 网络组件
                       channel: 'network', // 网络组件
                       channelId: networkCurrent,
                       channelId: networkCurrent,
                     };
                     };
-                    service.update(params).then((resp: any) => {
+                    service.update(param).then((resp: any) => {
                       if (resp.status === 200) {
                       if (resp.status === 200) {
                         message.success('操作成功!');
                         message.success('操作成功!');
-                        history.push(`${getMenuPathByCode(MENUS_CODE['link/AccessConfig'])}`);
+                        history.goBack();
+                        if ((window as any).onTabSaveSuccess) {
+                          (window as any).onTabSaveSuccess(resp);
+                          setTimeout(() => window.close(), 300);
+                        }
                       }
                       }
                     });
                     });
                   }
                   }

+ 3 - 17
src/pages/link/AccessConfig/Detail/index.tsx

@@ -1,30 +1,17 @@
 import { PageContainer } from '@ant-design/pro-layout';
 import { PageContainer } from '@ant-design/pro-layout';
-import { useEffect, useState } from 'react';
+import { useState } from 'react';
 import { useLocation } from 'umi';
 import { useLocation } from 'umi';
 import Access from './Access';
 import Access from './Access';
 import Provider from './Provider';
 import Provider from './Provider';
-import { service } from '@/pages/link/AccessConfig';
 
 
 type LocationType = {
 type LocationType = {
   id?: string;
   id?: string;
 };
 };
 
 
 const Detail = () => {
 const Detail = () => {
-  const [visible, setVisible] = useState<boolean>(true);
-  const [data, setData] = useState<any>({});
-  const [config, setConfig] = useState<any>({});
-
   const location = useLocation<LocationType>();
   const location = useLocation<LocationType>();
-
-  useEffect(() => {
-    const params = new URLSearchParams(location.search);
-    if (params.get('id')) {
-      service.detail(params.get('id') || '').then((resp) => {
-        setConfig(resp.result);
-        setVisible(false);
-      });
-    }
-  }, []);
+  const [visible, setVisible] = useState<boolean>(!new URLSearchParams(location.search).get('id'));
+  const [data, setData] = useState<any>({});
 
 
   return (
   return (
     <PageContainer>
     <PageContainer>
@@ -38,7 +25,6 @@ const Detail = () => {
       ) : (
       ) : (
         <Access
         <Access
           data={data}
           data={data}
-          access={config}
           change={() => {
           change={() => {
             setVisible(true);
             setVisible(true);
           }}
           }}

+ 18 - 1
src/pages/link/Protocol/index.tsx

@@ -1,6 +1,6 @@
 import { PageContainer } from '@ant-design/pro-layout';
 import { PageContainer } from '@ant-design/pro-layout';
 import type { ProtocolItem } from '@/pages/link/Protocol/typings';
 import type { ProtocolItem } from '@/pages/link/Protocol/typings';
-import { useRef } from 'react';
+import { useEffect, useRef } from 'react';
 import type { ActionType, ProColumns } from '@jetlinks/pro-table';
 import type { ActionType, ProColumns } from '@jetlinks/pro-table';
 import { Badge, Button, message, Popconfirm, Tooltip } from 'antd';
 import { Badge, Button, message, Popconfirm, Tooltip } from 'antd';
 import { CheckCircleOutlined, DeleteOutlined, EditOutlined, StopOutlined } from '@ant-design/icons';
 import { CheckCircleOutlined, DeleteOutlined, EditOutlined, StopOutlined } from '@ant-design/icons';
@@ -11,6 +11,8 @@ import { CurdModel } from '@/components/BaseCrud/model';
 import Service from '@/pages/link/Protocol/service';
 import Service from '@/pages/link/Protocol/service';
 import { onFormValuesChange, registerValidateRules } from '@formily/core';
 import { onFormValuesChange, registerValidateRules } from '@formily/core';
 import { Store } from 'jetlinks-store';
 import { Store } from 'jetlinks-store';
+import { useLocation } from 'umi';
+import SystemConst from '@/utils/const';
 
 
 export const service = new Service('protocol');
 export const service = new Service('protocol');
 const Protocol = () => {
 const Protocol = () => {
@@ -281,6 +283,21 @@ const Protocol = () => {
     },
     },
   };
   };
 
 
+  const location = useLocation();
+
+  useEffect(() => {
+    if ((location as any).query?.save === 'true') {
+      CurdModel.add();
+    }
+    const subscription = Store.subscribe(SystemConst.BASE_UPDATE_DATA, (data) => {
+      if ((window as any).onTabSaveSuccess) {
+        (window as any).onTabSaveSuccess(data);
+        setTimeout(() => window.close(), 300);
+      }
+    });
+    return () => subscription.unsubscribe();
+  }, []);
+
   return (
   return (
     <PageContainer>
     <PageContainer>
       <BaseCrud
       <BaseCrud

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

@@ -703,6 +703,10 @@ const Save = observer(() => {
     if (response.status === 200) {
     if (response.status === 200) {
       message.success('保存成功');
       message.success('保存成功');
       history.back();
       history.back();
+      if ((window as any).onTabSaveSuccess) {
+        (window as any).onTabSaveSuccess(response);
+        setTimeout(() => window.close(), 300);
+      }
     }
     }
   };
   };
   return (
   return (

+ 15 - 4
src/pages/system/Menu/Detail/edit.tsx

@@ -102,7 +102,12 @@ export default (props: EditProps) => {
           <Title title={'基本信息'} />
           <Title title={'基本信息'} />
           <Row>
           <Row>
             <Col span={3}>
             <Col span={3}>
-              <Form.Item name={'icon'} label={'菜单图标'} required={true}>
+              <Form.Item
+                name={'icon'}
+                label={'菜单图标'}
+                required={true}
+                rules={[{ required: true, message: '请上传图标' }]}
+              >
                 <UploadImage disabled={disabled} style={{ width: 140, height: 130 }} />
                 <UploadImage disabled={disabled} style={{ width: 140, height: 130 }} />
               </Form.Item>
               </Form.Item>
             </Col>
             </Col>
@@ -116,7 +121,7 @@ export default (props: EditProps) => {
                       defaultMessage: '名称',
                       defaultMessage: '名称',
                     })}
                     })}
                     required={true}
                     required={true}
-                    rules={[{ required: true, message: '该字段是必填字段' }]}
+                    rules={[{ required: true, message: '请输入名称' }]}
                   >
                   >
                     <Input disabled={disabled} />
                     <Input disabled={disabled} />
                   </Form.Item>
                   </Form.Item>
@@ -129,7 +134,7 @@ export default (props: EditProps) => {
                       defaultMessage: '编码',
                       defaultMessage: '编码',
                     })}
                     })}
                     required={true}
                     required={true}
-                    rules={[{ required: true, message: '该字段是必填字段' }]}
+                    rules={[{ required: true, message: '请输入编码' }]}
                   >
                   >
                     <Input disabled={disabled} />
                     <Input disabled={disabled} />
                   </Form.Item>
                   </Form.Item>
@@ -145,7 +150,7 @@ export default (props: EditProps) => {
                     })}
                     })}
                     required={true}
                     required={true}
                     rules={[
                     rules={[
-                      { required: true, message: '该字段是必填字段' },
+                      { required: true, message: '请输入页面地址' },
                       { max: 120, message: '最多可输入120字符' },
                       { max: 120, message: '最多可输入120字符' },
                     ]}
                     ]}
                   >
                   >
@@ -252,6 +257,12 @@ export default (props: EditProps) => {
                   />
                   />
                   {/*</Form.Item>*/}
                   {/*</Form.Item>*/}
                 </Form.Item>
                 </Form.Item>
+                <Form.Item hidden name={'id'}>
+                  <Input />
+                </Form.Item>
+                <Form.Item hidden name={'parentId'}>
+                  <Input />
+                </Form.Item>
               </Col>
               </Col>
             </Row>
             </Row>
           )}
           )}