xieyonghong 3 лет назад
Родитель
Сommit
56cf378fb0
69 измененных файлов с 2758 добавлено и 178 удалено
  1. BIN
      public/images/channel/1.png
  2. BIN
      public/images/channel/2.png
  3. BIN
      public/images/channel/3.png
  4. BIN
      public/images/channel/4.png
  5. BIN
      public/images/channel/background.png
  6. BIN
      public/images/network/CTWing.jpg
  7. BIN
      public/images/network/OneNet.jpg
  8. BIN
      public/images/network/doeros.jpg
  9. BIN
      public/images/running/doc.png
  10. BIN
      public/images/running/docx.png
  11. BIN
      public/images/running/flv.png
  12. BIN
      public/images/running/jpg.png
  13. BIN
      public/images/running/mp3.png
  14. BIN
      public/images/running/mp4.png
  15. BIN
      public/images/running/mvb.png
  16. BIN
      public/images/running/other.png
  17. BIN
      public/images/running/pdf.png
  18. BIN
      public/images/running/png.png
  19. BIN
      public/images/running/ppt.png
  20. BIN
      public/images/running/pptx.png
  21. BIN
      public/images/running/rmvb.png
  22. BIN
      public/images/running/swf.png
  23. BIN
      public/images/running/tiff.png
  24. BIN
      public/images/running/txt.png
  25. BIN
      public/images/running/wma.png
  26. BIN
      public/images/running/xls.png
  27. BIN
      public/images/running/xlsx.png
  28. 2 2
      src/app.tsx
  29. 3 1
      src/components/RightContent/AvatarDropdown.tsx
  30. 76 0
      src/pages/account/Center/bind/index.tsx
  31. 110 0
      src/pages/account/Center/edit/infoEdit.tsx
  32. 234 0
      src/pages/account/Center/edit/passwordEdit.tsx
  33. 29 0
      src/pages/account/Center/index.less
  34. 294 0
      src/pages/account/Center/index.tsx
  35. 66 0
      src/pages/account/Center/service.ts
  36. 21 0
      src/pages/account/Center/typings.d.ts
  37. 47 0
      src/pages/device/Instance/Detail/MetadataLog/Property/Detail.tsx
  38. 319 34
      src/pages/device/Instance/Detail/MetadataLog/Property/index.tsx
  39. 40 0
      src/pages/device/Instance/Detail/Running/Property/FileComponent/Detail.tsx
  40. 25 0
      src/pages/device/Instance/Detail/Running/Property/FileComponent/index.less
  41. 87 0
      src/pages/device/Instance/Detail/Running/Property/FileComponent/index.tsx
  42. 2 3
      src/pages/device/Instance/Detail/Running/Property/PropertyCard.tsx
  43. 2 1
      src/pages/device/Instance/Detail/Running/Property/index.tsx
  44. 11 0
      src/pages/device/Instance/service.ts
  45. 25 29
      src/pages/device/components/Metadata/Base/Edit/index.tsx
  46. 129 0
      src/pages/link/AccessConfig/Detail/Channel/index.tsx
  47. 23 0
      src/pages/link/AccessConfig/Detail/Cloud/CTWing/index.less
  48. 97 0
      src/pages/link/AccessConfig/Detail/Cloud/CTWing/index.tsx
  49. 134 0
      src/pages/link/AccessConfig/Detail/Cloud/Finish/index.tsx
  50. 23 0
      src/pages/link/AccessConfig/Detail/Cloud/OneNet/index.less
  51. 130 0
      src/pages/link/AccessConfig/Detail/Cloud/OneNet/index.tsx
  52. 18 0
      src/pages/link/AccessConfig/Detail/Cloud/Protocol/index.less
  53. 157 0
      src/pages/link/AccessConfig/Detail/Cloud/Protocol/index.tsx
  54. 81 0
      src/pages/link/AccessConfig/Detail/Cloud/index.less
  55. 117 0
      src/pages/link/AccessConfig/Detail/Cloud/index.tsx
  56. 69 83
      src/pages/link/AccessConfig/Detail/Provider/index.tsx
  57. 36 2
      src/pages/link/AccessConfig/Detail/index.tsx
  58. 26 0
      src/pages/link/Channel/Opcua/index.less
  59. 241 0
      src/pages/link/Channel/Opcua/index.tsx
  60. 4 0
      src/pages/link/Channel/Opcua/typings.d.ts
  61. 1 0
      src/pages/notice/Config/Detail/index.tsx
  62. 1 0
      src/pages/notice/Template/Detail/index.tsx
  63. 22 16
      src/pages/rule-engine/Alarm/Configuration/Save/index.tsx
  64. 1 1
      src/pages/rule-engine/Alarm/Log/TabComponent/index.tsx
  65. 9 0
      src/pages/rule-engine/Scene/Save/index.tsx
  66. 1 1
      src/pages/rule-engine/Scene/Save/trigger/index.tsx
  67. 19 4
      src/pages/rule-engine/Scene/index.tsx
  68. 23 1
      src/utils/menu/index.ts
  69. 3 0
      src/utils/menu/router.ts

BIN
public/images/channel/1.png


BIN
public/images/channel/2.png


BIN
public/images/channel/3.png


BIN
public/images/channel/4.png


BIN
public/images/channel/background.png


BIN
public/images/network/CTWing.jpg


BIN
public/images/network/OneNet.jpg


BIN
public/images/network/doeros.jpg


BIN
public/images/running/doc.png


BIN
public/images/running/docx.png


BIN
public/images/running/flv.png


BIN
public/images/running/jpg.png


BIN
public/images/running/mp3.png


BIN
public/images/running/mp4.png


BIN
public/images/running/mvb.png


BIN
public/images/running/other.png


BIN
public/images/running/pdf.png


BIN
public/images/running/png.png


BIN
public/images/running/ppt.png


BIN
public/images/running/pptx.png


BIN
public/images/running/rmvb.png


BIN
public/images/running/swf.png


BIN
public/images/running/tiff.png


BIN
public/images/running/txt.png


BIN
public/images/running/wma.png


BIN
public/images/running/xls.png


BIN
public/images/running/xlsx.png


+ 2 - 2
src/app.tsx

@@ -13,7 +13,7 @@ import type { RequestOptionsInit } from 'umi-request';
 import ReconnectingWebSocket from 'reconnecting-websocket';
 import SystemConst from '@/utils/const';
 import { service as MenuService } from '@/pages/system/Menu';
-import getRoutes, { getMenus, handleRoutes, saveMenusCache } from '@/utils/menu';
+import getRoutes, { extraRouteArr, getMenus, handleRoutes, saveMenusCache } from '@/utils/menu';
 import { AIcon } from '@/components';
 
 const isDev = process.env.NODE_ENV === 'development';
@@ -264,7 +264,7 @@ export function render(oldRender: any) {
             url: '/demo',
           });
         }
-        extraRoutes = handleRoutes(res.result);
+        extraRoutes = handleRoutes([...extraRouteArr, ...res.result]);
         saveMenusCache(extraRoutes);
       }
       oldRender();

+ 3 - 1
src/components/RightContent/AvatarDropdown.tsx

@@ -8,6 +8,7 @@ import styles from './index.less';
 import type { MenuInfo } from 'rc-menu/lib/interface';
 import { useIntl } from '@@/plugin-locale/localeExports';
 import Service from '@/pages/user/Login/service';
+import { getMenuPathByCode } from '@/utils/menu';
 
 export type GlobalHeaderRightProps = {
   menu?: boolean;
@@ -47,7 +48,8 @@ const AvatarDropdown: React.FC<GlobalHeaderRightProps> = ({ menu }) => {
         await loginOut();
         return;
       }
-      history.push(`/account/${key}`);
+      // console.log(key)
+      history.push(getMenuPathByCode('account/Center'));
     },
     [initialState, setInitialState],
   );

+ 76 - 0
src/pages/account/Center/bind/index.tsx

@@ -0,0 +1,76 @@
+import { Button, Card, Col, message, Row } from 'antd';
+import { useEffect, useState } from 'react';
+import Service from '@/pages/account/Center/service';
+
+export const service = new Service();
+
+const Bind = () => {
+  const [bindUser, setBindUser] = useState<any>();
+  const [user, setUser] = useState<any>();
+  const [code, setCode] = useState<string>('');
+
+  const bindUserInfo = (params: string) => {
+    service.bindUserInfo(params).then((res) => {
+      if (res.status === 200) {
+        setBindUser(res.result);
+      }
+    });
+  };
+  const getDetail = () => {
+    service.getUserDetail().subscribe((res) => {
+      setUser(res.result);
+    });
+  };
+
+  useEffect(() => {
+    // window.open('http://z.jetlinks.cn')
+    // const item = `http://pro.jetlinks.cn/#/user/login?sso=true&code=4decc08bcb87f3a4fbd74976fd86cd3d&redirect=http://pro.jetlinks.cn/jetlinks`;
+    const params = window.location.href.split('?')[1].split('&')[1].split('=')[1];
+    setCode(params);
+    bindUserInfo(params);
+    getDetail();
+  }, []);
+  return (
+    <>
+      <Card>
+        <Row gutter={[24, 24]}>
+          <Col span={12}>
+            <Card title="个人信息">
+              <p>登录账号:{user?.name}</p>
+              <p>姓名:{user?.name}</p>
+            </Card>
+          </Col>
+          <Col span={12}>
+            <Card title="三方账号信息">
+              <p>类型:{bindUser?.type}</p>
+              <p>组织:{bindUser?.providerName}</p>
+            </Card>
+          </Col>
+        </Row>
+        <Row gutter={[24, 24]}>
+          <Col span={24} style={{ textAlign: 'center', marginTop: 20 }}>
+            <Button
+              type="primary"
+              onClick={() => {
+                service.bind(code).then((res) => {
+                  if (res.status === 200) {
+                    message.success('绑定成功');
+                    if ((window as any).onBindSuccess) {
+                      (window as any).onBindSuccess(res);
+                      setTimeout(() => window.close(), 300);
+                    }
+                  } else {
+                    message.error('绑定失败');
+                  }
+                });
+              }}
+            >
+              立即绑定
+            </Button>
+          </Col>
+        </Row>
+      </Card>
+    </>
+  );
+};
+export default Bind;

+ 110 - 0
src/pages/account/Center/edit/infoEdit.tsx

@@ -0,0 +1,110 @@
+import { Modal, Form, Input, Col, Row } from 'antd';
+
+interface Props {
+  data: any;
+  save: Function;
+  close: Function;
+}
+
+const InfoEdit = (props: Props) => {
+  const [form] = Form.useForm();
+  const { data } = props;
+  const handleSave = async () => {
+    const formData = await form.validateFields();
+    console.log(formData);
+    props.save({
+      name: formData.name,
+      email: formData.email || '',
+      telephone: formData.telephone || '',
+    });
+  };
+
+  return (
+    <Modal
+      title="编辑"
+      visible
+      width="40vw"
+      destroyOnClose
+      onOk={handleSave}
+      onCancel={() => {
+        props.close();
+      }}
+    >
+      <Form
+        form={form}
+        layout="vertical"
+        initialValues={{
+          name: data.name,
+          username: data.username,
+          role: data?.roleList[0]?.name,
+          org: data?.orgList[0]?.name,
+          telephone: data.telephone,
+          email: data.email,
+        }}
+      >
+        <Row gutter={[24, 24]}>
+          <Col span={12}>
+            <Form.Item
+              label="姓名"
+              required
+              name="name"
+              rules={[
+                { type: 'string', max: 64 },
+                { required: true, message: '姓名必填' },
+              ]}
+            >
+              <Input placeholder="请输入姓名" />
+            </Form.Item>
+          </Col>
+          <Col span={12}>
+            <Form.Item label="用户名" name="username">
+              <Input placeholder="请输入用户名" disabled />
+            </Form.Item>
+          </Col>
+        </Row>
+        <Row gutter={[24, 24]}>
+          <Col span={12}>
+            <Form.Item label="角色" name="role">
+              <Input placeholder="请输入姓名" disabled />
+            </Form.Item>
+          </Col>
+          <Col span={12}>
+            <Form.Item label="部门" name="org">
+              <Input placeholder="请输入用户名" disabled />
+            </Form.Item>
+          </Col>
+        </Row>
+        <Row gutter={[24, 24]}>
+          <Col span={12}>
+            <Form.Item
+              label="手机号"
+              name="telephone"
+              rules={[
+                {
+                  pattern: /^1[3456789]\d{9}$/,
+                  message: '请输入正确手机号',
+                },
+              ]}
+            >
+              <Input placeholder="请输入手机号" />
+            </Form.Item>
+          </Col>
+          <Col span={12}>
+            <Form.Item
+              label="邮箱"
+              name="email"
+              rules={[
+                {
+                  type: 'email',
+                },
+              ]}
+            >
+              <Input placeholder="请输入邮箱" />
+            </Form.Item>
+          </Col>
+        </Row>
+      </Form>
+    </Modal>
+  );
+};
+export default InfoEdit;

+ 234 - 0
src/pages/account/Center/edit/passwordEdit.tsx

@@ -0,0 +1,234 @@
+import { Modal } from 'antd';
+import { createSchemaField } from '@formily/react';
+import { Form, FormItem, Password, Input } from '@formily/antd';
+import { ISchema } from '@formily/json-schema';
+import { useIntl } from 'umi';
+import { useMemo } from 'react';
+import { createForm } from '@formily/core';
+import { service } from '@/pages/account/Center';
+
+interface Props {
+  visible: boolean;
+  close: Function;
+  save: Function;
+}
+
+const PasswordEdit = (props: Props) => {
+  const intl = useIntl();
+  const SchemaField = createSchemaField({
+    components: {
+      FormItem,
+      Password,
+      Input,
+    },
+  });
+
+  const schema: ISchema = {
+    type: 'object',
+    properties: {
+      oldPassword: {
+        type: 'string',
+        title: '旧密码',
+        'x-decorator': 'FormItem',
+        'x-component': 'Input',
+        'x-component-props': {
+          checkStrength: true,
+          placeholder: '请输入旧密码',
+        },
+        required: true,
+        'x-validator': [
+          // {
+          //   max: 128,
+          //   message: '密码最多可输入128位',
+          // },
+          // {
+          //   min: 8,
+          //   message: '密码不能少于8位',
+          // },
+          {
+            required: true,
+            message: '请输入密码',
+          },
+          {
+            triggerType: 'onBlur',
+            validator: (value: string) => {
+              return new Promise((resolve) => {
+                service
+                  .validatePassword(value)
+                  .then((resp) => {
+                    if (resp.status === 200) {
+                      if (resp.result.passed) {
+                        resolve('');
+                      } else {
+                        resolve(resp.result.reason);
+                      }
+                    }
+                    resolve('');
+                  })
+                  .catch(() => {
+                    return '验证失败!';
+                  });
+              });
+            },
+          },
+        ],
+      },
+      newPassword: {
+        type: 'string',
+        title: intl.formatMessage({
+          id: 'pages.system.password',
+          defaultMessage: '密码',
+        }),
+        'x-decorator': 'FormItem',
+        'x-component': 'Password',
+        'x-component-props': {
+          checkStrength: true,
+          placeholder: '请输入密码',
+        },
+        required: true,
+
+        'x-reactions': [
+          {
+            dependencies: ['.confirmPassword'],
+            fulfill: {
+              state: {
+                selfErrors:
+                  '{{$deps[0] && $self.value && $self.value !==$deps[0] ? "两次密码输入不一致" : ""}}',
+              },
+            },
+          },
+        ],
+        name: 'password',
+        'x-validator': [
+          // {
+          //   max: 128,
+          //   message: '密码最多可输入128位',
+          // },
+          // {
+          //   min: 8,
+          //   message: '密码不能少于8位',
+          // },
+          {
+            required: true,
+            message: '请输入密码',
+          },
+          {
+            triggerType: 'onBlur',
+            validator: (value: string) => {
+              return new Promise((resolve) => {
+                service
+                  .validateField('password', value)
+                  .then((resp) => {
+                    if (resp.status === 200) {
+                      if (resp.result.passed) {
+                        resolve('');
+                      } else {
+                        resolve(resp.result.reason);
+                      }
+                    }
+                    resolve('');
+                  })
+                  .catch(() => {
+                    return '验证失败!';
+                  });
+              });
+            },
+          },
+        ],
+      },
+      confirmPassword: {
+        type: 'string',
+        title: intl.formatMessage({
+          id: 'pages.system.confirmPassword',
+          defaultMessage: '确认密码?',
+        }),
+        'x-decorator': 'FormItem',
+        'x-component': 'Password',
+        'x-component-props': {
+          checkStrength: true,
+          placeholder: '请再次输入密码',
+        },
+        'x-validator': [
+          // {
+          //   max: 128,
+          //   message: '密码最多可输入128位',
+          // },
+          // {
+          //   min: 8,
+          //   message: '密码不能少于8位',
+          // },
+          {
+            required: true,
+            message: '请输入确认密码',
+          },
+          {
+            triggerType: 'onBlur',
+            validator: (value: string) => {
+              return new Promise((resolve) => {
+                service
+                  .validateField('password', value)
+                  .then((resp) => {
+                    if (resp.status === 200) {
+                      if (resp.result.passed) {
+                        resolve('');
+                      } else {
+                        resolve(resp.result.reason);
+                      }
+                    }
+                    resolve('');
+                  })
+                  .catch(() => {
+                    return '验证失败!';
+                  });
+              });
+            },
+          },
+        ],
+        'x-reactions': [
+          {
+            dependencies: ['.password'],
+            fulfill: {
+              state: {
+                selfErrors:
+                  '{{$deps[0] && $self.value && $self.value !== $deps[0] ? "两次密码输入不一致" : ""}}',
+              },
+            },
+          },
+        ],
+        'x-decorator-props': {},
+        name: 'confirmPassword',
+      },
+    },
+  };
+
+  const form = useMemo(() => createForm({}), [props.visible]);
+  return (
+    <Modal
+      title="重置密码"
+      visible
+      onCancel={() => props.close()}
+      onOk={async () => {
+        const value: { oldPassword: string; newPassword: string; confirmPassword: string } =
+          await form.submit();
+        console.log(value);
+        props.save({
+          oldPassword: value.oldPassword,
+          newPassword: value.newPassword,
+        });
+        // if (props.data.id) {
+        //   const resp = await service.resetPassword(props.data.id, value.confirmPassword);
+        //   if (resp.status === 200) {
+        //     message.success('操作成功');
+        //     props.close();
+        //   }
+        // }
+        // props.close();
+      }}
+    >
+      <Form form={form} layout="vertical">
+        <SchemaField schema={schema} />
+      </Form>
+    </Modal>
+  );
+};
+export default PasswordEdit;

+ 29 - 0
src/pages/account/Center/index.less

@@ -0,0 +1,29 @@
+.info {
+  margin-top: 15px;
+}
+.top {
+  display: flex;
+  width: 100%;
+  .avatar {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: space-between;
+    width: 350px;
+    height: 200px;
+  }
+  .content {
+    width: 80%;
+    padding-top: 15px;
+  }
+  .action {
+    position: relative;
+    top: 15px;
+    right: 20px;
+  }
+}
+.bind {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+}

+ 294 - 0
src/pages/account/Center/index.tsx

@@ -0,0 +1,294 @@
+import { PageContainer } from '@ant-design/pro-layout';
+import { Card, Upload, message, Avatar, Button, Descriptions, Divider, Col, Row } from 'antd';
+import { useEffect, useState } from 'react';
+import styles from './index.less';
+import { UploadProps } from 'antd/lib/upload';
+import Token from '@/utils/token';
+import SystemConst from '@/utils/const';
+import { EditOutlined, LockOutlined, UploadOutlined, UserOutlined } from '@ant-design/icons';
+import InfoEdit from './edit/infoEdit';
+import PasswordEdit from './edit/passwordEdit';
+import Service from '@/pages/account/Center/service';
+import moment from 'moment';
+
+export const service = new Service();
+
+const Center = () => {
+  const [data, setData] = useState<any>();
+  const [imageUrl, setImageUrl] = useState<string>('');
+  // const [loading, setLoading] = useState<boolean>(false)
+  const [infos, setInfos] = useState<boolean>(false);
+  const [password, setPassword] = useState<boolean>(false);
+  const [bindList, setBindList] = useState<any>([]);
+
+  const iconMap = new Map();
+  iconMap.set('dingtalk', require('/public/images/notice/dingtalk.png'));
+  iconMap.set('wechat-webapp', require('/public/images/notice/wechat.png'));
+
+  const bGroundMap = new Map();
+  bGroundMap.set('dingtalk', require('/public/images/notice/dingtalk-background.png'));
+  bGroundMap.set('wechat-webapp', require('/public/images/notice/wechat-background.png'));
+
+  const uploadProps: UploadProps = {
+    showUploadList: false,
+    action: `/${SystemConst.API_BASE}/file/static`,
+    headers: {
+      'X-Access-Token': Token.get(),
+    },
+    beforeUpload(file) {
+      const isJpgOrPng = file.type === 'image/jpeg' || file.type === 'image/png';
+      if (!isJpgOrPng) {
+        message.error('请上传.png.jpg格式的文件');
+      }
+      return isJpgOrPng;
+    },
+    onChange(info) {
+      if (info.file.status === 'uploading') {
+        // setLoading(true);
+      }
+      if (info.file.status === 'done') {
+        setImageUrl(info.file.response.result);
+        service
+          .saveUserDetail({
+            name: data.name,
+            avatar: info.file.response.result,
+          })
+          .subscribe((res) => {
+            if (res.status === 200) {
+              setImageUrl(info.file.response.result);
+              message.success('上传成功');
+            }
+          });
+        // setLoading(false);
+      }
+    },
+  };
+
+  const getDetail = () => {
+    service.getUserDetail().subscribe((res) => {
+      setData(res.result);
+      setImageUrl(res.result.avatar);
+    });
+  };
+  const saveInfo = (parms: UserDetail) => {
+    service.saveUserDetail(parms).subscribe((res) => {
+      if (res.status === 200) {
+        message.success('保存成功');
+        getDetail();
+        setInfos(false);
+      } else {
+        message.success('保存失败');
+      }
+    });
+  };
+  const savePassword = (parms: { oldPassword: string; newPassword: string }) => {
+    service.savePassWord(parms).subscribe((res) => {
+      if (res.status === 200) {
+        message.success('保存成功');
+      }
+    });
+  };
+  const getBindInfo = () => {
+    service.bindInfo().then((res) => {
+      if (res.status === 200) {
+        console.log(res);
+        setBindList(res.result);
+      }
+    });
+  };
+  const unBind = (type: string, provider: string) => {
+    service.unbind(type, provider).then((res) => {
+      if (res.status === 200) {
+        message.success('解绑成功');
+        getBindInfo();
+      }
+    });
+  };
+
+  useEffect(() => {
+    getDetail();
+    getBindInfo();
+  }, []);
+
+  return (
+    <PageContainer>
+      <Card>
+        <div className={styles.top}>
+          <div className={styles.avatar}>
+            <div>
+              {data?.avatar ? (
+                <Avatar size={140} src={imageUrl} />
+              ) : (
+                <Avatar size={140} icon={<UserOutlined />} />
+              )}
+            </div>
+            <Upload {...uploadProps}>
+              <Button>
+                <UploadOutlined />
+                更换头像
+              </Button>
+            </Upload>
+          </div>
+          <div className={styles.content}>
+            <Descriptions column={4} layout="vertical">
+              <Descriptions.Item label="登录账号">{data?.username}</Descriptions.Item>
+              <Descriptions.Item label="账号ID">{data?.id}</Descriptions.Item>
+              <Descriptions.Item label="注册时间">
+                {moment(data?.createTime).format('YYYY-MM-DD HH:mm:ss')}
+              </Descriptions.Item>
+              <Descriptions.Item label="电话">{data?.telephone || '-'}</Descriptions.Item>
+              <Descriptions.Item label="姓名">{data?.name}</Descriptions.Item>
+              <Descriptions.Item label="角色">{data?.roleList[0]?.name || '-'}</Descriptions.Item>
+              <Descriptions.Item label="部门">{data?.orgList[0]?.name || '-'}</Descriptions.Item>
+              <Descriptions.Item label="邮箱">{data?.email || '-'}</Descriptions.Item>
+            </Descriptions>
+          </div>
+          <a>
+            {' '}
+            <EditOutlined
+              className={styles.action}
+              onClick={() => {
+                setInfos(true);
+              }}
+            />
+          </a>
+        </div>
+      </Card>
+      <Card
+        className={styles.info}
+        title={
+          <div style={{ fontSize: '22px' }}>
+            <Divider type="vertical" style={{ backgroundColor: '#2F54EB', width: 3 }} />
+            修改密码
+          </div>
+        }
+        extra={
+          <a>
+            {' '}
+            <EditOutlined
+              onClick={() => {
+                setPassword(true);
+              }}
+            />
+          </a>
+        }
+      >
+        <div style={{ display: 'flex', alignItems: 'flex-end' }}>
+          <div>
+            <LockOutlined
+              style={{
+                color: '#1d39c4',
+                fontSize: '70px',
+              }}
+            />
+          </div>
+          <div style={{ marginLeft: 5, color: 'rgba(0, 0, 0, 0.55)' }}>
+            安全性高的密码可以使帐号更安全。建议您定期更换密码,设置一个包含字母,符号或数字中至少两项且长度超过8位的密码
+          </div>
+        </div>
+      </Card>
+      <Card
+        className={styles.info}
+        title={
+          <div style={{ fontSize: '22px' }}>
+            <Divider type="vertical" style={{ backgroundColor: '#2F54EB', width: 3 }} />
+            绑定三方账号
+          </div>
+        }
+      >
+        <Row gutter={[24, 24]}>
+          {bindList.map((item: any) => (
+            <Col key={item.type}>
+              <Card
+                style={{
+                  background: `url(${bGroundMap.get(item.type)}) no-repeat`,
+                  backgroundSize: '100% 100%',
+                  width: 415,
+                }}
+              >
+                <div className={styles.bind}>
+                  <div>
+                    <img style={{ height: 56 }} src={iconMap.get(item.type)} />
+                  </div>
+                  <div>
+                    {item.bound ? (
+                      <div>
+                        <div style={{ fontSize: '22px' }}>绑定名:{item.others.name}</div>
+                        <div
+                          style={{
+                            fontSize: '14px',
+                            lineHeight: '20px',
+                            marginTop: '5px',
+                            color: '#00000073',
+                          }}
+                        >
+                          绑定时间: 2022-01-02 09:00:00
+                        </div>
+                      </div>
+                    ) : (
+                      <div style={{ fontSize: '22px' }}>{`${
+                        item.type === 'dingtalk' ? '钉钉' : '微信'
+                      }未绑定`}</div>
+                    )}
+                  </div>
+                  <div>
+                    {item.bound ? (
+                      <Button
+                        onClick={() => {
+                          unBind(item.type, item.provider);
+                        }}
+                      >
+                        解除绑定
+                      </Button>
+                    ) : (
+                      <Button
+                        type="primary"
+                        onClick={() => {
+                          const items: any = window.open(
+                            `/${SystemConst.API_BASE}/sso/${item.provider}/login`,
+                          );
+                          //  const items:any= window.open(`/#/account/Center/bind`);
+                          items!.onBindSuccess = (value: any) => {
+                            if (value.status === 200) {
+                              getBindInfo();
+                            }
+                          };
+                          // history.push(getMenuPathByCode('account/Center/bind'));
+                        }}
+                      >
+                        立即绑定
+                      </Button>
+                    )}
+                  </div>
+                </div>
+              </Card>
+            </Col>
+          ))}
+        </Row>
+      </Card>
+      {infos && (
+        <InfoEdit
+          data={data}
+          save={(item: any) => {
+            saveInfo(item);
+          }}
+          close={() => {
+            setInfos(false);
+          }}
+        />
+      )}
+      {password && (
+        <PasswordEdit
+          save={(item: any) => {
+            savePassword(item);
+          }}
+          visible={password}
+          close={() => {
+            setPassword(false);
+          }}
+        />
+      )}
+    </PageContainer>
+  );
+};
+export default Center;

+ 66 - 0
src/pages/account/Center/service.ts

@@ -0,0 +1,66 @@
+import BaseService from '@/utils/BaseService';
+import { request } from 'umi';
+import { defer, from } from 'rxjs';
+import { map } from 'rxjs/operators';
+import SystemConst from '@/utils/const';
+
+class Service extends BaseService<UserItem> {
+  getUserDetail = (params?: any) =>
+    defer(() =>
+      from(
+        request(`/${SystemConst.API_BASE}/user/detail`, {
+          method: 'GET',
+          params,
+        }),
+      ),
+    ).pipe(map((item) => item));
+
+  saveUserDetail = (data: any) =>
+    defer(() =>
+      from(
+        request(`/${SystemConst.API_BASE}/user/detail`, {
+          method: 'PUT',
+          data,
+        }),
+      ),
+    ).pipe(map((item) => item));
+  savePassWord = (data: any) =>
+    defer(() =>
+      from(
+        request(`/${SystemConst.API_BASE}/user/passwd`, {
+          method: 'PUT',
+          data,
+        }),
+      ),
+    ).pipe(map((item) => item));
+
+  validateField = (type: 'username' | 'password', name: string) =>
+    request(`/${SystemConst.API_BASE}/user/${type}/_validate`, {
+      method: 'POST',
+      data: name,
+    });
+  validatePassword = (password: string) =>
+    request(`/${SystemConst.API_BASE}/user/me/password/_validate`, {
+      method: 'POST',
+      data: password,
+    });
+  bindInfo = (params?: any) =>
+    request(`/${SystemConst.API_BASE}/sso/me/bindings`, {
+      method: 'GET',
+      params,
+    });
+  bindUserInfo = (code: string) =>
+    request(`/${SystemConst.API_BASE}/sso/bind-code/${code}`, {
+      method: 'GET',
+    });
+  bind = (code: string) =>
+    request(`/${SystemConst.API_BASE}/sso/me/bind/${code}`, {
+      method: 'POST',
+    });
+  unbind = (type: string, provider: string) =>
+    request(`/${SystemConst.API_BASE}/sso/me/${type}/${provider}/unbind`, {
+      method: 'POST',
+    });
+}
+
+export default Service;

+ 21 - 0
src/pages/account/Center/typings.d.ts

@@ -0,0 +1,21 @@
+type UserItem = {
+  id: string;
+  name: string;
+  status: number;
+  username: string;
+  createTime: number;
+  email?: string;
+  telephone?: string;
+  avatar?: string;
+  description?: string;
+  orgList?: { id: string; name: string }[] | string[];
+  roleList?: { id: string; name: string }[] | string[];
+  orgIdList?: string[];
+  roleIdList?: string[];
+};
+type UserDetail = {
+  name: string;
+  emmail?: string;
+  telephone?: string;
+  avatar?: string;
+};

+ 47 - 0
src/pages/device/Instance/Detail/MetadataLog/Property/Detail.tsx

@@ -0,0 +1,47 @@
+import { Modal, Input } from 'antd';
+// import ReactMarkdown from "react-markdown";
+
+interface Props {
+  close: () => void;
+  value: any;
+  type: string;
+}
+
+const Detail = (props: Props) => {
+  const { value, type } = props;
+
+  const renderValue = () => {
+    if (type === 'object') {
+      return (
+        <div>
+          <div>自定义属性</div>
+          {JSON.stringify(value)}
+        </div>
+      );
+    } else {
+      return (
+        <div>
+          <div>自定义属性</div>
+          <Input value={value} disabled />
+        </div>
+      );
+    }
+  };
+
+  return (
+    <Modal
+      title="详情"
+      visible
+      onOk={() => {
+        props.close();
+      }}
+      onCancel={() => {
+        props.close();
+      }}
+    >
+      {renderValue()}
+    </Modal>
+  );
+};
+
+export default Detail;

+ 319 - 34
src/pages/device/Instance/Detail/MetadataLog/Property/index.tsx

@@ -1,11 +1,14 @@
 import { service } from '@/pages/device/Instance';
 import { useParams } from 'umi';
-import { DatePicker, Modal, Radio, Space, Table } from 'antd';
+import { DatePicker, Modal, Radio, Select, Space, Table, Tabs } from 'antd';
 import type { PropertyMetadata } from '@/pages/device/Product/typings';
 import encodeQuery from '@/utils/encodeQuery';
 import { useEffect, useState } from 'react';
 import moment from 'moment';
-
+import { Axis, Chart, Geom, Legend, Tooltip, Slider } from 'bizcharts';
+import FileComponent from '../../Running/Property/FileComponent';
+import { DownloadOutlined, SearchOutlined } from '@ant-design/icons';
+import Detail from './Detail';
 interface Props {
   visible: boolean;
   close: () => void;
@@ -15,11 +18,20 @@ interface Props {
 const PropertyLog = (props: Props) => {
   const params = useParams<{ id: string }>();
   const { visible, close, data } = props;
+  const list = ['int', 'float', 'double', 'long'];
   const [dataSource, setDataSource] = useState<any>({});
   const [start, setStart] = useState<number>(moment().startOf('day').valueOf());
   const [end, setEnd] = useState<number>(new Date().getTime());
   const [radioValue, setRadioValue] = useState<undefined | 'today' | 'week' | 'month'>('today');
   const [dateValue, setDateValue] = useState<any>(undefined);
+  const [chartsList, setChartsList] = useState<any>([]);
+  const [cycle, setCycle] = useState<string>(
+    list.includes(data.valueType?.type || '') ? '*' : '1m',
+  );
+  const [agg, setAgg] = useState<string>('AVG');
+  const [tab, setTab] = useState<string>('table');
+  const [detailVisible, setDetailVisible] = useState<boolean>(false);
+  const [current, setCurrent] = useState<any>('');
 
   const columns = [
     {
@@ -29,9 +41,42 @@ const PropertyLog = (props: Props) => {
       render: (text: any) => <span>{text ? moment(text).format('YYYY-MM-DD HH:mm:ss') : ''}</span>,
     },
     {
-      title: '自定义属性',
-      dataIndex: 'formatValue',
-      key: 'formatValue',
+      title: <span>{data.valueType?.type !== 'file' ? '自定义属性' : '文件内容'}</span>,
+      dataIndex: 'value',
+      key: 'value',
+      render: (text: any, record: any) => (
+        <FileComponent type="table" value={{ formatValue: record.value }} data={data} />
+      ),
+    },
+    {
+      title: '操作',
+      dataIndex: 'action',
+      key: 'action',
+      render: (text: any, record: any) => (
+        <a>
+          {data.valueType?.type !== 'file' ? (
+            <SearchOutlined
+              onClick={() => {
+                setDetailVisible(true);
+                setCurrent(record.value);
+              }}
+            />
+          ) : (
+            <DownloadOutlined />
+          )}
+        </a>
+      ),
+    },
+  ];
+
+  const tabList = [
+    {
+      tab: '列表',
+      key: 'table',
+    },
+    {
+      tab: '图表',
+      key: 'charts',
     },
   ];
 
@@ -55,7 +100,47 @@ const PropertyLog = (props: Props) => {
       });
   };
 
+  const queryChartsList = async (startTime?: number, endTime?: number) => {
+    const resp = await service.queryPropertieList(params.id, data.id || '', {
+      paging: false,
+      terms: [
+        {
+          column: 'timestamp$BTW',
+          value: startTime && endTime ? [startTime, endTime] : [],
+          type: 'and',
+        },
+      ],
+    });
+    if (resp.status === 200) {
+      const dataList: any[] = [];
+      resp.result.data.forEach((i: any) => {
+        dataList.push({
+          year: moment(i.timestamp).format('YYYY-MM-DD HH:mm:ss'),
+          value: i.value,
+          type: data?.name || '',
+        });
+      });
+      setChartsList(dataList);
+    }
+  };
+
+  const queryChartsAggList = async (datas: any) => {
+    const resp = await service.queryPropertieInfo(params.id, datas);
+    if (resp.status === 200) {
+      const dataList: any[] = [];
+      resp.result.forEach((i: any) => {
+        dataList.push({
+          year: moment(i.time).format('YYYY-MM-DD HH:mm:ss'),
+          value: Number(i[data.id || '']),
+          type: data?.name || '',
+        });
+      });
+      setChartsList(dataList);
+    }
+  };
+
   useEffect(() => {
+    console.log(data);
     if (visible) {
       handleSearch(
         {
@@ -68,6 +153,162 @@ const PropertyLog = (props: Props) => {
     }
   }, [visible]);
 
+  const scale = {
+    value: { min: 0 },
+    year: {
+      range: [0, 0.96],
+      type: 'timeCat',
+    },
+  };
+
+  const renderComponent = (type: string) => {
+    switch (type) {
+      case 'table':
+        return (
+          <Table
+            size="small"
+            rowKey={'id'}
+            onChange={(page) => {
+              handleSearch(
+                {
+                  pageSize: page.pageSize,
+                  pageIndex: Number(page.current) - 1 || 0,
+                },
+                start,
+                end,
+              );
+            }}
+            dataSource={dataSource?.data || []}
+            columns={columns}
+            pagination={{
+              pageSize: dataSource?.pageSize || 10,
+              showSizeChanger: true,
+              total: dataSource?.total || 0,
+            }}
+          />
+        );
+      case 'charts':
+        return (
+          <div>
+            <div style={{ margin: '10 0', display: 'flex' }}>
+              <div style={{ marginRight: 20 }}>
+                统计周期:
+                <Select
+                  value={cycle}
+                  style={{ width: 120 }}
+                  onChange={(value: string) => {
+                    setCycle(value);
+                    if (cycle === '*') {
+                      queryChartsList(start, end);
+                    } else {
+                      queryChartsAggList({
+                        columns: [
+                          {
+                            property: data.id,
+                            alias: data.id,
+                            agg: agg,
+                          },
+                        ],
+                        query: {
+                          interval: value,
+                          format: 'yyyy-MM-dd HH:mm:ss',
+                          from: start,
+                          to: end,
+                        },
+                      });
+                    }
+                  }}
+                >
+                  {list.includes(data.valueType?.type || '') && (
+                    <Select.Option value="*">实际值</Select.Option>
+                  )}
+                  <Select.Option value="1m">按分钟统计</Select.Option>
+                  <Select.Option value="1h">按小时统计</Select.Option>
+                  <Select.Option value="1d">按天统计</Select.Option>
+                  <Select.Option value="1w">按周统计</Select.Option>
+                  <Select.Option value="1M">按月统计</Select.Option>
+                </Select>
+              </div>
+              {cycle !== '*' && list.includes(data.valueType?.type || '') && (
+                <div>
+                  统计规则:
+                  <Select
+                    defaultValue="AVG"
+                    style={{ width: 120 }}
+                    onChange={(value: string) => {
+                      setAgg(value);
+                      queryChartsAggList({
+                        columns: [
+                          {
+                            property: data.id,
+                            alias: data.id,
+                            agg: value,
+                          },
+                        ],
+                        query: {
+                          interval: cycle,
+                          format: 'yyyy-MM-dd HH:mm:ss',
+                          from: start,
+                          to: end,
+                        },
+                      });
+                    }}
+                  >
+                    <Select.Option value="AVG">平均值</Select.Option>
+                    <Select.Option value="MAX">最大值</Select.Option>
+                    <Select.Option value="MIN">最小值</Select.Option>
+                    <Select.Option value="COUNT">总数</Select.Option>
+                  </Select>
+                </div>
+              )}
+            </div>
+            <div style={{ paddingTop: 15 }}>
+              <Chart height={400} data={chartsList} scale={scale} autoFit>
+                <Legend />
+                <Axis name="year" />
+                <Axis
+                  name="value"
+                  label={{
+                    formatter: (val) => parseFloat(val).toLocaleString(),
+                  }}
+                />
+                <Tooltip showCrosshairs shared />
+                <Geom
+                  type="line"
+                  tooltip={[
+                    'value*type',
+                    (value, name) => {
+                      return {
+                        value: value,
+                        name,
+                      };
+                    },
+                  ]}
+                  position="year*value"
+                  size={2}
+                />
+                <Geom
+                  type="point"
+                  tooltip={false}
+                  position="year*value"
+                  size={4}
+                  shape={'circle'}
+                  style={{
+                    stroke: '#fff',
+                    lineWidth: 1,
+                  }}
+                />
+                <Geom type="area" position="year*value" shape={'circle'} tooltip={false} />
+                <Slider />
+              </Chart>
+            </div>
+          </div>
+        );
+      default:
+        return null;
+    }
+  };
+
   // @ts-ignore
   return (
     <Modal
@@ -98,14 +339,36 @@ const PropertyLog = (props: Props) => {
               setDateValue(undefined);
               setStart(st);
               setEnd(et);
-              handleSearch(
-                {
-                  pageSize: 10,
-                  pageIndex: 0,
-                },
-                st,
-                et,
-              );
+              if (tab === 'charts') {
+                if (list.includes(data.valueType?.type || '')) {
+                  queryChartsList(st, et);
+                } else {
+                  queryChartsAggList({
+                    columns: [
+                      {
+                        property: data.id,
+                        alias: data.id,
+                        agg,
+                      },
+                    ],
+                    query: {
+                      interval: cycle,
+                      format: 'yyyy-MM-dd HH:mm:ss',
+                      from: st,
+                      to: et,
+                    },
+                  });
+                }
+              } else {
+                handleSearch(
+                  {
+                    pageSize: 10,
+                    pageIndex: 0,
+                  },
+                  st,
+                  et,
+                );
+              }
             }}
             style={{ minWidth: 220 }}
           >
@@ -140,28 +403,50 @@ const PropertyLog = (props: Props) => {
           }
         </Space>
       </div>
-
-      <Table
-        size="small"
-        rowKey={'id'}
-        onChange={(page) => {
-          handleSearch(
-            {
-              pageSize: page.pageSize,
-              pageIndex: Number(page.current) - 1 || 0,
-            },
-            start,
-            end,
-          );
-        }}
-        dataSource={dataSource?.data || []}
-        columns={columns}
-        pagination={{
-          pageSize: dataSource?.pageSize || 10,
-          showSizeChanger: true,
-          total: dataSource?.total || 0,
+      <Tabs
+        activeKey={tab}
+        onChange={(key: string) => {
+          setTab(key);
+          if (key === 'charts' && !!data.valueType?.type) {
+            if (list.includes(data.valueType?.type)) {
+              queryChartsList(start, end);
+            } else {
+              setCycle('1m');
+              setAgg('COUNT');
+              queryChartsAggList({
+                columns: [
+                  {
+                    property: data.id,
+                    alias: data.id,
+                    agg: 'COUNT',
+                  },
+                ],
+                query: {
+                  interval: '1m',
+                  format: 'yyyy-MM-dd HH:mm:ss',
+                  from: start,
+                  to: end,
+                },
+              });
+            }
+          }
         }}
-      />
+      >
+        {tabList.map((item) => (
+          <Tabs.TabPane tab={item.tab} key={item.key}>
+            {renderComponent(item.key)}
+          </Tabs.TabPane>
+        ))}
+      </Tabs>
+      {detailVisible && (
+        <Detail
+          close={() => {
+            setDetailVisible(false);
+          }}
+          value={current}
+          type={data.valueType?.type || ''}
+        />
+      )}
     </Modal>
   );
 };

+ 40 - 0
src/pages/device/Instance/Detail/Running/Property/FileComponent/Detail.tsx

@@ -0,0 +1,40 @@
+import LivePlayer from '@/components/Player';
+import { Modal, Image } from 'antd';
+
+interface Props {
+  close: () => void;
+  value: any;
+  type: string;
+}
+
+const Detail = (props: Props) => {
+  const { value, type } = props;
+
+  const renderValue = () => {
+    if (['jpg', 'png', 'tiff'].includes(type)) {
+      return <Image src={value?.formatValue} />;
+    } else if (value?.formatValue.indexOf('https') !== -1) {
+      return <p>域名为https时,不支持访问http地址</p>;
+    } else if (['flv', 'm3u8', 'mp4'].includes(type)) {
+      return <LivePlayer live={false} url={value?.formatValue} />;
+    }
+    return <p>当前仅支持播放.mp4,.flv,.m3u8格式的视频</p>;
+  };
+
+  return (
+    <Modal
+      title="详情"
+      visible
+      onOk={() => {
+        props.close();
+      }}
+      onCancel={() => {
+        props.close();
+      }}
+    >
+      {renderValue()}
+    </Modal>
+  );
+};
+
+export default Detail;

+ 25 - 0
src/pages/device/Instance/Detail/Running/Property/FileComponent/index.less

@@ -0,0 +1,25 @@
+.value {
+  display: flex;
+  align-items: center;
+  width: 100%;
+  height: 60px;
+
+  .other {
+    width: 100%;
+    overflow: hidden;
+    color: #323130;
+    font-weight: 700;
+    font-size: 24px;
+    white-space: nowrap;
+    text-overflow: ellipsis;
+  }
+
+  .img {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    width: 60px;
+    height: 100%;
+    border: 1px solid rgba(0, 0, 0, 0.08);
+  }
+}

+ 87 - 0
src/pages/device/Instance/Detail/Running/Property/FileComponent/index.tsx

@@ -0,0 +1,87 @@
+import type { PropertyMetadata } from '@/pages/device/Product/typings';
+import styles from './index.less';
+import Detail from './Detail';
+import { useState } from 'react';
+
+interface Props {
+  data: Partial<PropertyMetadata>;
+  value: any;
+  type: 'card' | 'table';
+}
+
+const imgMap = new Map<any, any>();
+imgMap.set('txt', require('/public/images/running/txt.png'));
+imgMap.set('doc', require('/public/images/running/doc.png'));
+imgMap.set('xls', require('/public/images/running/xls.png'));
+imgMap.set('ppt', require('/public/images/running/ppt.png'));
+imgMap.set('docx', require('/public/images/running/docx.png'));
+imgMap.set('xlsx', require('/public/images/running/xlsx.png'));
+imgMap.set('pptx', require('/public/images/running/pptx.png'));
+imgMap.set('jpg', require('/public/images/running/jpg.png'));
+imgMap.set('png', require('/public/images/running/png.png'));
+imgMap.set('pdf', require('/public/images/running/pdf.png'));
+imgMap.set('tiff', require('/public/images/running/tiff.png'));
+imgMap.set('swf', require('/public/images/running/swf.png'));
+imgMap.set('flv', require('/public/images/running/flv.png'));
+imgMap.set('rmvb', require('/public/images/running/rmvb.png'));
+imgMap.set('mp4', require('/public/images/running/mp4.png'));
+imgMap.set('mvb', require('/public/images/running/mvb.png'));
+imgMap.set('wma', require('/public/images/running/wma.png'));
+imgMap.set('mp3', require('/public/images/running/mp3.png'));
+imgMap.set('other', require('/public/images/running/other.png'));
+
+const FileComponent = (props: Props) => {
+  const { data, value } = props;
+  const [type, setType] = useState<string>('other');
+  const [visible, setVisible] = useState<boolean>(false);
+
+  const renderValue = () => {
+    if (!value?.formatValue) {
+      return <div className={props.type === 'card' ? styles.other : {}}>--</div>;
+    } else if (data?.valueType?.type === 'file') {
+      const flag: string = value?.formatValue.split('.').pop() || 'other';
+      return (
+        <div
+          className={styles.img}
+          onClick={() => {
+            if (['jpg', 'png', 'tiff', 'flv', 'm3u8', 'mp4', 'rmvb', 'mvb'].includes(flag)) {
+              setType(flag);
+              setVisible(true);
+            }
+          }}
+        >
+          <img src={imgMap.get(flag) || imgMap.get('other')} />
+        </div>
+      );
+    } else if (data?.valueType?.type === 'object') {
+      return (
+        <div className={props.type === 'card' ? styles.other : {}}>
+          {JSON.stringify(value?.formatValue)}
+        </div>
+      );
+    } else {
+      return (
+        <div className={props.type === 'card' ? styles.other : {}}>
+          {String(value?.formatValue)}
+        </div>
+      );
+    }
+  };
+
+  return (
+    <div className={styles.value}>
+      {renderValue()}
+      {visible && (
+        <Detail
+          type={type}
+          value={value}
+          close={() => {
+            setVisible(false);
+          }}
+        />
+      )}
+    </div>
+  );
+};
+
+export default FileComponent;

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

@@ -14,6 +14,7 @@ import EditProperty from '@/pages/device/Instance/Detail/Running/Property/EditPr
 import moment from 'moment';
 import Indicators from './Indicators';
 import './PropertyCard.less';
+import FileComponent from './FileComponent';
 
 interface Props {
   data: Partial<PropertyMetadata>;
@@ -88,9 +89,7 @@ const Property = (props: Props) => {
       <Spin spinning={loading}>
         <div>
           <div>{renderTitle(data?.name || '')}</div>
-          <div className="value" style={{ fontWeight: 700, fontSize: '24px', color: '#323130' }}>
-            {value?.formatValue || '--'}
-          </div>
+          <FileComponent type="card" value={value} data={data} />
           <div style={{ marginTop: 10 }}>
             <div style={{ color: 'rgba(0, 0, 0, .65)', fontSize: 12 }}>更新时间</div>
             <div style={{ marginTop: 5, fontSize: 16, color: 'black' }} className="value">

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

@@ -12,6 +12,7 @@ import { useParams } from 'umi';
 import PropertyLog from '../../MetadataLog/Property';
 import moment from 'moment';
 import styles from './index.less';
+import FileComponent from './FileComponent';
 
 interface Props {
   data: Partial<PropertyMetadata>[];
@@ -64,7 +65,7 @@ const Property = (props: Props) => {
       dataIndex: 'value',
       key: 'value',
       render: (text: any, record: any) => (
-        <span>{propertyValue[record.id]?.formatValue || '--'}</span>
+        <FileComponent type="table" value={propertyValue[record.id]} data={record} />
       ),
     },
     {

+ 11 - 0
src/pages/device/Instance/service.ts

@@ -292,6 +292,17 @@ class Service extends BaseService<DeviceInstance> {
     request(`/${SystemConst.API_BASE}/device-instance/${deviceId}/metric/property/${propertyId}`, {
       method: 'GET',
     });
+  //聚合查询设备属性
+  public queryPropertieInfo = (deviceId: string, data: any) =>
+    request(`/${SystemConst.API_BASE}/device-instance/${deviceId}/agg/_query`, {
+      method: 'POST',
+      data,
+    });
+  public queryPropertieList = (deviceId: string, property: string, data: any) =>
+    request(`/${SystemConst.API_BASE}/device-instance/${deviceId}/property/${property}/_query`, {
+      method: 'POST',
+      data,
+    });
 }
 
 export default Service;

+ 25 - 29
src/pages/device/components/Metadata/Base/Edit/index.tsx

@@ -2,7 +2,13 @@ import { Button, Drawer, Dropdown, Menu, message } from 'antd';
 import { createSchemaField, observer } from '@formily/react';
 import MetadataModel from '../model';
 import type { Field, IFieldState } from '@formily/core';
-import { createForm, onFieldInit, onFieldReact, registerValidateRules } from '@formily/core';
+import {
+  createForm,
+  onFieldInit,
+  onFieldReact,
+  onFieldValueChange,
+  registerValidateRules,
+} from '@formily/core';
 import {
   ArrayItems,
   Checkbox,
@@ -57,8 +63,15 @@ const Edit = observer((props: Props) => {
   const form = useMemo(
     () =>
       createForm({
-        initialValues: MetadataModel.item as Record<string, unknown>,
+        initialValues: _.cloneDeep(MetadataModel.item as Record<string, unknown>),
         effects: () => {
+          onFieldValueChange('valueType.type', (field, form1) => {
+            if (field.modified) {
+              form1.setFieldState('expands.metrics', (state) => {
+                state.value = [];
+              });
+            }
+          });
           onFieldInit('expands.metrics.*.id', (field) => {
             const id = field as Field;
             if (id.value && !id.modified) {
@@ -96,7 +109,7 @@ const Edit = observer((props: Props) => {
           });
         },
       }),
-    [],
+    [MetadataModel.edit],
   );
 
   const schemaTitleMapping = {
@@ -838,6 +851,7 @@ const Edit = observer((props: Props) => {
                   visible:
                     props.type === 'product' &&
                     "{{['int','float','double','long','date','string','boolean'].includes($deps[0])}}",
+                  // value: []
                 },
               },
             },
@@ -969,31 +983,24 @@ const Edit = observer((props: Props) => {
     );
   };
 
-  // const param = useParams<{ id: string }>();
   const typeMap = new Map<string, any>();
 
   typeMap.set('product', productModel.current);
   typeMap.set('device', InstanceModel.detail);
-  // const saveMap = new Map<string, Promise<any>>();
   const { type } = MetadataModel;
 
   const saveMetadata = async (deploy?: boolean) => {
     setLoading(true);
-    const params = (await form.submit()) as MetadataItem;
+    let params;
+    try {
+      params = (await form.submit()) as MetadataItem;
+    } catch (e) {
+      setLoading(false);
+      return;
+    }
 
     if (!typeMap.get(props.type)) return;
 
-    // const metadata = JSON.parse(typeMap.get(props.type).metadata || '{}') as DeviceMetadata;
-    // const config = (metadata[type] || []) as MetadataItem[];
-    // const index = config.findIndex((item) => item.id === params.id);
-    // if (index > -1) {
-    //   config[index] = params;
-    //   DB.getDB().table(`${type}`).update(params.id, params);
-    // } else {
-    //   config.push(params);
-    //   DB.getDB().table(`${type}`).add(params, params.id);
-    // }
-
     const updateDB = (t: 'add' | 'update', item: MetadataItem) => {
       switch (t) {
         case 'add':
@@ -1006,18 +1013,7 @@ const Edit = observer((props: Props) => {
     };
 
     const _data = updateMetadata(type, [params], typeMap.get(props.type), updateDB);
-    // console.log(params, JSON.parse(_data.metadata));
-    // if (props.type === 'product') {
-    //   // const product = typeMap.get('product');
-    //   // @ts-ignore
-    //   // metadata[type] = config;
-    //   // product.metadata = JSON.stringify(metadata);
-    //   saveMap.set('product', service.saveProductMetadata(_data));
-    // } else {
-    //   saveMap.set('device', service.saveDeviceMetadata(param.id, { metadata: _data.metadata }));
-    // }
-    //
-    // const result = await saveMap.get(props.type);
+
     const result = await asyncUpdateMedata(props.type, _data);
     if (result.status === 200) {
       if ((window as any).onTabSaveSuccess) {

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

@@ -0,0 +1,129 @@
+import { Button, Card, Col, Form, Input, message, Row } from 'antd';
+import { useEffect, useState } from 'react';
+import { service } from '@/pages/link/AccessConfig';
+import { ProcotoleMapping } from '../Cloud/Protocol';
+import TitleComponent from '@/components/TitleComponent';
+import { getButtonPermission } from '@/utils/menu';
+import ReactMarkdown from 'react-markdown';
+import { useHistory } from 'umi';
+
+interface Props {
+  change: () => void;
+  data: any;
+  provider: any;
+}
+
+const Media = (props: Props) => {
+  const [form] = Form.useForm();
+  const [config, setConfig] = useState<any>({});
+  const history = useHistory();
+
+  const procotol = props.provider.id === 'modbus-tcp' ? 'modbus-tcp' : 'opc-ua';
+  const name = props.provider.id === 'modbus-tcp' ? 'Modbus' : 'OPCUA';
+
+  useEffect(() => {
+    form.setFieldsValue({
+      name: props.data.name,
+      description: props.data.description,
+    });
+  }, [props.data]);
+
+  useEffect(() => {
+    console.log(ProcotoleMapping);
+    service.getConfigView(procotol, ProcotoleMapping.get(props.provider?.id)).then((resp) => {
+      if (resp.status === 200) {
+        setConfig(resp.result);
+      }
+    });
+  }, [props.provider]);
+
+  return (
+    <Card>
+      {!props.data?.id && (
+        <Button
+          type="link"
+          onClick={() => {
+            props.change();
+          }}
+        >
+          返回
+        </Button>
+      )}
+      <div style={{ margin: '20px 30px' }}>
+        <Row gutter={24}>
+          <Col span={12}>
+            <div>
+              <TitleComponent data={'基本信息'} />
+              <Form name="basic" layout="vertical" form={form}>
+                <Form.Item
+                  label="名称"
+                  name="name"
+                  rules={[{ required: true, message: '请输入名称' }]}
+                >
+                  <Input placeholder="请输入名称" />
+                </Form.Item>
+                <Form.Item name="description" label="说明">
+                  <Input.TextArea showCount maxLength={200} placeholder="请输入说明" />
+                </Form.Item>
+              </Form>
+              <div style={{ marginTop: 50 }}>
+                <Button
+                  type="primary"
+                  disabled={
+                    !!props.data.id
+                      ? getButtonPermission('link/AccessConfig', ['update'])
+                      : getButtonPermission('link/AccessConfig', ['add'])
+                  }
+                  onClick={async () => {
+                    try {
+                      const values = await form.validateFields();
+                      const param = {
+                        ...props.data,
+                        ...values,
+                        provider: props.provider?.id,
+                        protocol: procotol,
+                        transport: props.provider.id === 'modbus-tcp' ? 'MODBUS_TCP' : 'OPC_UA',
+                        channel: props.provider.id === 'modbus-tcp' ? 'modbus' : 'opc-ua',
+                      };
+                      const resp: any = await service[!props.data?.id ? 'save' : 'update'](param);
+                      if (resp.status === 200) {
+                        message.success('操作成功!');
+                        history.goBack();
+                      }
+                    } catch (errorInfo) {
+                      console.error('Failed:', errorInfo);
+                    }
+                  }}
+                >
+                  保存
+                </Button>
+              </div>
+            </div>
+          </Col>
+          <Col span={12}>
+            <div style={{ marginLeft: 10 }}>
+              <TitleComponent data={'配置概览'} />
+              <div>
+                <p>接入方式:{props.provider?.name || '--'}</p>
+                {props.provider?.description && <p>{props.provider?.description || '--'}</p>}
+                <p>消息协议:{procotol}</p>
+                {config?.document && (
+                  <div>{<ReactMarkdown>{config?.document}</ReactMarkdown> || '--'}</div>
+                )}
+              </div>
+              <TitleComponent data={'设备接入指引'} />
+              <div>
+                <p>1、配置{name}通道</p>
+                <p>2、创建{name}设备接入网关</p>
+                <p>3、创建产品,并选中接入方式为{name}</p>
+                <p>4、添加设备,单独为每一个设备进行数据点绑定</p>
+              </div>
+            </div>
+          </Col>
+        </Row>
+      </div>
+    </Card>
+  );
+};
+
+export default Media;

+ 23 - 0
src/pages/link/AccessConfig/Detail/Cloud/CTWing/index.less

@@ -0,0 +1,23 @@
+.doc {
+  height: 550px;
+  padding: 24px;
+  overflow-y: auto;
+  color: rgba(#000, 0.8);
+  font-size: 14px;
+  background-color: #fafafa;
+
+  h1 {
+    margin: 16px 0;
+    color: rgba(#000, 0.85);
+    font-weight: bold;
+    font-size: 14px;
+
+    &:first-child {
+      margin-top: 0;
+    }
+  }
+
+  .image {
+    margin: 16px 0;
+  }
+}

+ 97 - 0
src/pages/link/AccessConfig/Detail/Cloud/CTWing/index.tsx

@@ -0,0 +1,97 @@
+import { Button, Col, Form, Input, Row, Image } from 'antd';
+import { useEffect } from 'react';
+import styles from './index.less';
+
+interface Props {
+  next: (data: any) => void;
+  data: any;
+}
+
+const CTWing = (props: Props) => {
+  const [form] = Form.useForm();
+  const img = require('/public/images/network/CTWing.jpg');
+
+  useEffect(() => {
+    form.setFieldsValue({
+      ...props.data,
+      apiAddress: 'https://ag-api.ctwing.cn/',
+    });
+  }, [props.data]);
+
+  return (
+    <Row gutter={24}>
+      <Col span={16}>
+        <Form
+          name="CTWing"
+          layout="vertical"
+          form={form}
+          initialValues={{
+            apiAddress: 'https://ag-api.ctwing.cn/',
+          }}
+          onFinish={(values: any) => {
+            props.next(values);
+          }}
+        >
+          <Row gutter={24}>
+            <Col span={12}>
+              <Form.Item
+                label="接口地址"
+                name="apiAddress"
+                rules={[{ required: true, message: '请输入接口地址' }]}
+              >
+                <Input disabled placeholder="请输入接口地址" />
+              </Form.Item>
+            </Col>
+            <Col span={12}>
+              <Form.Item
+                label="appKey"
+                name="appKey"
+                rules={[{ required: true, message: '请输入appKey' }]}
+              >
+                <Input placeholder="请输入appKey" />
+              </Form.Item>
+            </Col>
+            <Col span={12}>
+              <Form.Item
+                label="appSecret"
+                name="appSecret"
+                rules={[{ required: true, message: '请输入appSecret' }]}
+              >
+                <Input placeholder="请输入appSecret" />
+              </Form.Item>
+            </Col>
+            <Col span={24}>
+              <Form.Item name="description" label="说明">
+                <Input.TextArea showCount maxLength={200} placeholder="请输入说明" />
+              </Form.Item>
+            </Col>
+            <Col>
+              <Form.Item>
+                <Button type="primary" htmlType="submit">
+                  下一步
+                </Button>
+              </Form.Item>
+            </Col>
+          </Row>
+        </Form>
+      </Col>
+      <Col span={8}>
+        <div className={styles.doc}>
+          <h1>操作指引:</h1>
+          <div>1、创建类型为CTWing的设备接入网关</div>
+          <div>2、创建产品,并选中接入方式为CTWing</div>
+          <div>
+            3、添加设备,为每一台设备设置唯一的IMEI、SN、PSK码(需与CTWingt平台中填写的值一致,若CTWing平台没有对应的设备,将会通过CTWing平台提供的LWM2M协议自动创建)
+          </div>
+          <div className={styles.image}>
+            <Image width="100%" src={img} />
+          </div>
+          <h1>配置说明</h1>
+          <div>1.请将CTWing的AEP平台-应用管理中的App Key和App Secret复制到当前页面</div>
+        </div>
+      </Col>
+    </Row>
+  );
+};
+
+export default CTWing;

+ 134 - 0
src/pages/link/AccessConfig/Detail/Cloud/Finish/index.tsx

@@ -0,0 +1,134 @@
+import { TitleComponent } from '@/components';
+import { getButtonPermission } from '@/utils/menu';
+import { Button, Col, Form, Input, message, Row } from 'antd';
+import { service } from '@/pages/link/AccessConfig';
+import { useHistory } from 'umi';
+import { useEffect, useState } from 'react';
+import ReactMarkdown from 'react-markdown';
+import { ProcotoleMapping } from '../Protocol';
+
+interface Props {
+  prev: () => void;
+  data: any;
+  config: any;
+  provider: any;
+  procotol: string;
+}
+
+const Finish = (props: Props) => {
+  const [form] = Form.useForm();
+  const history = useHistory();
+  const [config, setConfig] = useState<any>({});
+
+  useEffect(() => {
+    form.setFieldsValue({
+      name: props.data.name,
+      description: props.data.description,
+    });
+  }, [props.data]);
+
+  useEffect(() => {
+    service.getConfigView(props.procotol, ProcotoleMapping.get(props.provider?.id)).then((resp) => {
+      if (resp.status === 200) {
+        setConfig(resp.result);
+      }
+    });
+  }, [props.procotol, props.provider]);
+
+  return (
+    <Row gutter={24}>
+      <Col span={12}>
+        <div>
+          <TitleComponent data={'基本信息'} />
+          <Form name="basic" layout="vertical" form={form}>
+            <Form.Item label="名称" name="name" rules={[{ required: true, message: '请输入名称' }]}>
+              <Input placeholder="请输入名称" />
+            </Form.Item>
+            <Form.Item name="description" label="说明">
+              <Input.TextArea showCount maxLength={200} placeholder="请输入说明" />
+            </Form.Item>
+          </Form>
+          <div style={{ marginTop: 50 }}>
+            <Button
+              style={{ margin: '0 8px' }}
+              onClick={() => {
+                props.prev();
+              }}
+            >
+              上一步
+            </Button>
+            <Button
+              type="primary"
+              disabled={
+                !!props.data.id
+                  ? getButtonPermission('link/AccessConfig', ['update'])
+                  : getButtonPermission('link/AccessConfig', ['add'])
+              }
+              onClick={async () => {
+                try {
+                  const values = await form.validateFields();
+                  const param = {
+                    ...props.data,
+                    ...values,
+                    provider: props.provider.id,
+                    protocol: props.procotol,
+                    transport: 'HTTP_SERVER',
+                    configuration: {
+                      ...props.config,
+                    },
+                  };
+                  const resp: any = await service[!props.data?.id ? 'save' : 'update'](param);
+                  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);
+                }
+              }}
+            >
+              保存
+            </Button>
+          </div>
+        </div>
+      </Col>
+      <Col span={12}>
+        <div style={{ marginLeft: 10 }}>
+          <TitleComponent data={'配置概览'} />
+          <div>
+            <p>接入方式:{props.provider?.name || '--'}</p>
+            {props.provider?.description && <p>{props.provider?.description || '--'}</p>}
+            <p>消息协议:{props.procotol}</p>
+            {config?.document && (
+              <div>{<ReactMarkdown>{config?.document}</ReactMarkdown> || '--'}</div>
+            )}
+          </div>
+          <TitleComponent data={'设备接入指引'} />
+          <div>
+            <p>
+              1、创建类型为{props?.provider?.id === 'OneNet' ? 'OneNet' : 'CTWing'}的设备接入网关
+            </p>
+            <p>
+              2、创建产品,并选中接入方式为{props?.provider?.id === 'OneNet' ? 'OneNet' : 'CTWing'}
+            </p>
+            {props?.provider?.id === 'OneNet' ? (
+              <p>
+                3、添加设备,为每一台设备设置唯一的IMEI、IMSI码(需与OneNet平台中填写的值一致,若OneNet平台没有对应的设备,将会通过OneNet平台提供的LWM2M协议自动创建)
+              </p>
+            ) : (
+              <p>
+                3、添加设备,为每一台设备设置唯一的IMEI、SN、PSK码(需与CTWingt平台中填写的值一致,若CTWing平台没有对应的设备,将会通
+              </p>
+            )}
+          </div>
+        </div>
+      </Col>
+    </Row>
+  );
+};
+
+export default Finish;

+ 23 - 0
src/pages/link/AccessConfig/Detail/Cloud/OneNet/index.less

@@ -0,0 +1,23 @@
+.doc {
+  height: 550px;
+  padding: 24px;
+  overflow-y: auto;
+  color: rgba(#000, 0.8);
+  font-size: 14px;
+  background-color: #fafafa;
+
+  h1 {
+    margin: 16px 0;
+    color: rgba(#000, 0.85);
+    font-weight: bold;
+    font-size: 14px;
+
+    &:first-child {
+      margin-top: 0;
+    }
+  }
+
+  .image {
+    margin: 16px 0;
+  }
+}

+ 130 - 0
src/pages/link/AccessConfig/Detail/Cloud/OneNet/index.tsx

@@ -0,0 +1,130 @@
+import { QuestionCircleOutlined } from '@ant-design/icons';
+import { Button, Col, Form, Input, Row, Tooltip, Image } from 'antd';
+import { useEffect } from 'react';
+import styles from './index.less';
+
+interface Props {
+  next: (data: any) => void;
+  data: any;
+}
+
+const OneNet = (props: Props) => {
+  const img = require('/public/images/network/OneNet.jpg');
+
+  const [form] = Form.useForm();
+
+  useEffect(() => {
+    form.setFieldsValue({
+      ...props.data,
+      apiAddress: 'https://ag-api.ctwing.cn/',
+    });
+  }, [props.data]);
+
+  return (
+    <Row gutter={24}>
+      <Col span={16}>
+        <Form
+          name="onenet"
+          layout="vertical"
+          form={form}
+          initialValues={{
+            apiAddress: 'https://api.heclouds.com/',
+          }}
+          onFinish={(values: any) => {
+            props.next(values);
+          }}
+        >
+          <Row gutter={24}>
+            <Col span={24}>
+              <Form.Item
+                label={
+                  <span>
+                    接口地址
+                    <Tooltip title={`同步物联网平台设备数据到OneNet`}>
+                      <QuestionCircleOutlined />
+                    </Tooltip>
+                  </span>
+                }
+                name="apiAddress"
+                rules={[{ required: true, message: '请输入接口地址' }]}
+              >
+                <Input disabled placeholder="请输入接口地址" />
+              </Form.Item>
+            </Col>
+            <Col span={24}>
+              <Form.Item
+                label={<span>apiKey</span>}
+                name="apiKey"
+                rules={[{ required: true, message: '请输入apiKey' }]}
+              >
+                <Input placeholder="请输入apiKey" />
+              </Form.Item>
+            </Col>
+            <Col span={12}>
+              <Form.Item
+                label={
+                  <span>
+                    通知Token
+                    <Tooltip title={`接收OneNet推送的Token地址`}>
+                      <QuestionCircleOutlined />
+                    </Tooltip>
+                  </span>
+                }
+                name="validateToken"
+                rules={[{ required: true, message: '请输入通知Token' }]}
+              >
+                <Input placeholder="请输入通知Token" />
+              </Form.Item>
+            </Col>
+            <Col span={12}>
+              <Form.Item
+                label={
+                  <span>
+                    aesKey
+                    <Tooltip title={`OneNet 端生成的消息加密key`}>
+                      <QuestionCircleOutlined />
+                    </Tooltip>
+                  </span>
+                }
+                name="aesKey"
+              >
+                <Input placeholder="请输入aesKey" />
+              </Form.Item>
+            </Col>
+            <Col span={24}>
+              <Form.Item name="description" label="说明">
+                <Input.TextArea showCount maxLength={200} placeholder="请输入说明" />
+              </Form.Item>
+            </Col>
+            <Col>
+              <Form.Item>
+                <Button type="primary" htmlType="submit">
+                  下一步
+                </Button>
+              </Form.Item>
+            </Col>
+          </Row>
+        </Form>
+      </Col>
+      <Col span={8}>
+        <div className={styles.doc}>
+          <h1>操作指引:</h1>
+          <div>1、创建类型为OneNet的设备接入网关</div>
+          <div>2、创建产品,并选中接入方式为OneNet</div>
+          <div>
+            3、添加设备,为每一台设备设置唯一的IMEI、IMSI码(需与OneNet平台中填写的值一致,若OneNet平台没有对应的设备,将会通过OneNet平台提供的LWM2M协议自动创建)
+          </div>
+          <div className={styles.image}>
+            <Image width="100%" src={img} />
+          </div>
+          <h1>配置说明</h1>
+          <div>1.接口地址需要与OneNet数据推送配置中地址一致</div>
+          <div>2.通知Token需要与OneNet数据推送配置中Token一致</div>
+          <div>3.aesKey需要与OneNet数据推送配置中aesKey一致</div>
+        </div>
+      </Col>
+    </Row>
+  );
+};
+
+export default OneNet;

+ 18 - 0
src/pages/link/AccessConfig/Detail/Cloud/Protocol/index.less

@@ -0,0 +1,18 @@
+.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%;
+}

+ 157 - 0
src/pages/link/AccessConfig/Detail/Cloud/Protocol/index.tsx

@@ -0,0 +1,157 @@
+import { getButtonPermission, getMenuPathByCode, MENUS_CODE } from '@/utils/menu';
+import { Button, Card, Col, Empty, Input, message, Row, Space } from 'antd';
+import { useEffect, useState } from 'react';
+import { service } from '@/pages/link/AccessConfig';
+import styles from './index.less';
+import PermissionButton from '@/components/PermissionButton';
+import encodeQuery from '@/utils/encodeQuery';
+
+export const ProcotoleMapping = new Map();
+ProcotoleMapping.set('websocket-server', 'WebSocket');
+ProcotoleMapping.set('http-server-gateway', 'HTTP');
+ProcotoleMapping.set('udp-device-gateway', 'UDP');
+ProcotoleMapping.set('coap-server-gateway', 'COAP');
+ProcotoleMapping.set('mqtt-client-gateway', 'MQTT');
+ProcotoleMapping.set('mqtt-server-gateway', 'MQTT');
+ProcotoleMapping.set('tcp-server-gateway', 'TCP');
+ProcotoleMapping.set('child-device', '');
+ProcotoleMapping.set('OneNet', 'OneNet');
+ProcotoleMapping.set('Ctwing', 'Ctwing');
+ProcotoleMapping.set('modbus-tcp', 'MODBUS_TCP');
+ProcotoleMapping.set('opc-ua', 'OPC_UA');
+
+interface Props {
+  provider: any;
+  data: string;
+  prev: () => void;
+  next: (data: string) => void;
+}
+
+const Protocol = (props: Props) => {
+  const [procotolList, setProcotolList] = useState<any[]>([]);
+  const [procotolCurrent, setProcotolCurrent] = useState<string>('');
+  const protocolPermission = PermissionButton.usePermission('link/Protocol').permission;
+
+  const queryProcotolList = (id?: string, params?: any) => {
+    service.getProtocolList(ProcotoleMapping.get(id), params).then((resp) => {
+      if (resp.status === 200) {
+        setProcotolList(resp.result);
+      }
+    });
+  };
+
+  useEffect(() => {
+    queryProcotolList(props.provider?.id);
+  }, [props.provider]);
+
+  useEffect(() => {
+    setProcotolCurrent(props.data);
+  }, [props.data]);
+
+  return (
+    <div>
+      <div className={styles.search}>
+        <Input.Search
+          key={'protocol'}
+          placeholder="请输入名称"
+          onSearch={(value: string) => {
+            queryProcotolList(
+              props.provider?.id,
+              encodeQuery({
+                terms: {
+                  name$LIKE: `%${value}%`,
+                },
+              }),
+            );
+          }}
+          style={{ width: 500, margin: '20px 0' }}
+        />
+        <PermissionButton
+          isPermission={protocolPermission.add}
+          onClick={() => {
+            const url = getMenuPathByCode(MENUS_CODE[`link/Protocol`]);
+            const tab: any = window.open(`${origin}/#${url}?save=true`);
+            tab!.onTabSaveSuccess = (resp: any) => {
+              if (resp.status === 200) {
+                queryProcotolList(props.provider?.id);
+              }
+            };
+          }}
+          key="button"
+          type="primary"
+        >
+          新增
+        </PermissionButton>
+      </div>
+      {procotolList.length > 0 ? (
+        <Row gutter={[16, 16]}>
+          {procotolList.map((item) => (
+            <Col key={item.id} span={8}>
+              <Card
+                className={styles.cardRender}
+                style={{
+                  width: '100%',
+                  borderColor: procotolCurrent === item.id ? 'var(--ant-primary-color-active)' : '',
+                }}
+                hoverable
+                onClick={() => {
+                  setProcotolCurrent(item.id);
+                }}
+              >
+                <div style={{ height: '45px' }}>
+                  <div className={styles.title}>{item.name || '--'}</div>
+                  <div className={styles.desc}>{item.description || '--'}</div>
+                </div>
+              </Card>
+            </Col>
+          ))}
+        </Row>
+      ) : (
+        <Empty
+          description={
+            <span>
+              暂无数据
+              {getButtonPermission('link/Protocol', ['add']) ? (
+                '请联系管理员进行配置'
+              ) : (
+                <Button
+                  type="link"
+                  onClick={() => {
+                    const url = getMenuPathByCode(MENUS_CODE[`link/Protocol`]);
+                    const tab: any = window.open(`${origin}/#${url}?save=true`);
+                    tab!.onTabSaveSuccess = (resp: any) => {
+                      if (resp.status === 200) {
+                        queryProcotolList(props.provider?.id);
+                      }
+                    };
+                  }}
+                >
+                  去新增
+                </Button>
+              )}
+            </span>
+          }
+        />
+      )}
+      <Space style={{ marginTop: 20 }}>
+        <Button style={{ margin: '0 8px' }} onClick={() => props.prev()}>
+          上一步
+        </Button>
+        <Button
+          type="primary"
+          onClick={() => {
+            if (!procotolCurrent) {
+              message.error('请选择消息协议!');
+            } else {
+              props.next(procotolCurrent);
+            }
+          }}
+        >
+          下一步
+        </Button>
+      </Space>
+    </div>
+  );
+};
+
+export default Protocol;

+ 81 - 0
src/pages/link/AccessConfig/Detail/Cloud/index.less

@@ -0,0 +1,81 @@
+.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;
+  }
+}
+
+.config {
+  padding: 10px 20px 20px 20px;
+  color: rgba(0, 0, 0, 0.8);
+  background: rgba(0, 0, 0, 0.04);
+
+  .title {
+    width: 100%;
+    margin: 10px 0;
+    font-weight: 600;
+  }
+
+  .item {
+    margin-bottom: 10px;
+
+    .context {
+      margin: 5px 0;
+      color: rgba(0, 0, 0, 0.8);
+    }
+  }
+}
+
+.alert {
+  height: 40px;
+  padding-left: 10px;
+  color: rgba(0, 0, 0, 0.55);
+  line-height: 40px;
+  background-color: #f6f6f6;
+}

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

@@ -0,0 +1,117 @@
+import { Button, Card, Steps } from 'antd';
+import { useEffect, useState } from 'react';
+import styles from './index.less';
+import { ExclamationCircleFilled } from '@ant-design/icons';
+import OneNet from './OneNet';
+import CTWing from './CTWing';
+import Protocol from './Protocol';
+import Finish from './Finish';
+
+interface Props {
+  change: () => void;
+  data: any;
+  provider: any;
+}
+
+const Cloud = (props: Props) => {
+  const [current, setCurrent] = useState<number>(0);
+  const [steps] = useState<string[]>(['接入配置', '消息协议', '完成']);
+  const [config, setConfig] = useState<any>({});
+  const [procotolCurrent, setProcotolCurrent] = useState<string>('');
+
+  const prev = () => {
+    setCurrent(current - 1);
+  };
+
+  const next = (param: any) => {
+    setConfig(param);
+    setCurrent(current + 1);
+  };
+
+  useEffect(() => {
+    setCurrent(0);
+    setConfig(props.data?.configuration || {});
+    setProcotolCurrent(props.data?.protocol);
+  }, [props.data]);
+
+  const renderSteps = (cur: number) => {
+    switch (cur) {
+      case 0:
+        return (
+          <div>
+            <div className={styles.alert}>
+              <ExclamationCircleFilled style={{ marginRight: 10 }} />
+              通过{props?.provider?.id === 'OneNet' ? 'OneNet' : 'CTWing'}
+              平台的HTTP推送服务进行数据接入
+            </div>
+            <div style={{ marginTop: 10 }}>
+              {props?.provider?.id === 'OneNet' ? (
+                <OneNet data={config} next={(param: any) => next(param)} />
+              ) : (
+                <CTWing data={config} next={(param: any) => next(param)} />
+              )}
+            </div>
+          </div>
+        );
+      case 1:
+        return (
+          <div>
+            <div className={styles.alert}>
+              <ExclamationCircleFilled style={{ marginRight: 10 }} />
+              只能选择HTTP通信方式的协议
+            </div>
+            <div style={{ marginTop: 10 }}>
+              <Protocol
+                data={procotolCurrent}
+                provider={props.provider}
+                next={(param: string) => {
+                  setProcotolCurrent(param);
+                  setCurrent(current + 1);
+                }}
+                prev={prev}
+              />
+            </div>
+          </div>
+        );
+      case 2:
+        return (
+          <Finish
+            procotol={procotolCurrent}
+            provider={props.provider}
+            data={props.data}
+            config={config}
+            prev={prev}
+          />
+        );
+      default:
+        return null;
+    }
+  };
+
+  return (
+    <Card>
+      {!props.data?.id && (
+        <Button
+          type="link"
+          onClick={() => {
+            props.change();
+          }}
+        >
+          返回
+        </Button>
+      )}
+      <div className={styles.box}>
+        <div className={styles.steps}>
+          <Steps size="small" current={current}>
+            {steps.map((item) => (
+              <Steps.Step key={item} title={item} />
+            ))}
+          </Steps>
+        </div>
+        <div className={styles.content}>{renderSteps(current)}</div>
+      </div>
+    </Card>
+  );
+};
+
+export default Cloud;

+ 69 - 83
src/pages/link/AccessConfig/Detail/Provider/index.tsx

@@ -1,6 +1,6 @@
+import { useEffect, useState } from 'react';
 import { TitleComponent } from '@/components';
 import { Button, Card, Col, Row } from 'antd';
-import { useEffect, useState } from 'react';
 import styles from './index.less';
 
 interface Props {
@@ -10,109 +10,95 @@ interface Props {
 
 const Provider = (props: Props) => {
   const [dataSource, setDataSource] = useState<any[]>([]);
-  const [mediaSource, setMediaSource] = useState<any[]>([]);
 
   useEffect(() => {
     const media: any[] = [];
-    const data: any = [];
+    const network: any[] = [];
+    const cloud: any[] = [];
+    const channel: any[] = [];
     (props?.data || []).map((item: any) => {
       if (item.id === 'fixed-media' || item.id === 'gb28181-2016') {
         media.push(item);
+      } else if (item.id === 'OneNet' || item.id === 'Ctwing') {
+        cloud.push(item);
+      } else if (item.id === 'modbus-tcp' || item.id === 'opc-ua') {
+        channel.push(item);
       } else {
-        data.push(item);
+        network.push(item);
       }
     });
-    setDataSource(data);
-    setMediaSource(media);
+
+    setDataSource([
+      {
+        type: 'network',
+        list: [...network],
+        title: '自定义设备接入',
+      },
+      {
+        type: 'media',
+        list: [...media],
+        title: '视频类设备接入',
+      },
+      {
+        type: 'cloud',
+        list: [...cloud],
+        title: '云平台接入',
+      },
+      {
+        type: 'channel',
+        list: [...channel],
+        title: '通道类设备接入',
+      },
+    ]);
   }, [props.data]);
 
   return (
-    <>
-      <Card>
-        <TitleComponent data={'自定义设备接入'} />
-        <Row gutter={[16, 16]}>
-          {dataSource.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>
+      {dataSource.map((i) => (
+        <Card key={i.type} style={{ marginTop: 20 }}>
+          <TitleComponent data={i.title} />
+          <Row gutter={[16, 16]}>
+            {(i?.list || []).map((item: any) => (
+              <Col key={item.name} span={12}>
+                <Card style={{ width: '100%' }} hoverable>
                   <div
                     style={{
+                      width: '100%',
                       display: 'flex',
-                      width: 'calc(100% - 70px)',
+                      alignItems: 'center',
+                      justifyContent: 'space-between',
                     }}
                   >
-                    <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, 'network');
+                    <div
+                      style={{
+                        display: 'flex',
+                        width: 'calc(100% - 70px)',
                       }}
                     >
-                      接入
-                    </Button>
-                  </div>
-                </div>
-              </Card>
-            </Col>
-          ))}
-        </Row>
-      </Card>
-      <Card style={{ marginTop: 20 }}>
-        <TitleComponent data={'视频类设备接入'} />
-        <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 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, i.type);
+                        }}
+                      >
+                        接入
+                      </Button>
                     </div>
                   </div>
-                  <div style={{ width: '70px' }}>
-                    <Button
-                      type="primary"
-                      onClick={() => {
-                        props.change(item, 'media');
-                      }}
-                    >
-                      接入
-                    </Button>
-                  </div>
-                </div>
-              </Card>
-            </Col>
-          ))}
-        </Row>
-      </Card>
-    </>
+                </Card>
+              </Col>
+            ))}
+          </Row>
+        </Card>
+      ))}
+    </div>
   );
 };
 

+ 36 - 2
src/pages/link/AccessConfig/Detail/index.tsx

@@ -6,6 +6,8 @@ import Provider from './Provider';
 import Media from './Media';
 import { service } from '@/pages/link/AccessConfig';
 import { Spin } from 'antd';
+import Cloud from './Cloud';
+import Channel from './Channel';
 
 type LocationType = {
   id?: string;
@@ -17,7 +19,9 @@ const Detail = () => {
   const [loading, setLoading] = useState<boolean>(true);
   const [data, setData] = useState<any>({});
   const [provider, setProvider] = useState<any>({});
-  const [type, setType] = useState<'media' | 'network' | undefined>(undefined);
+  const [type, setType] = useState<'media' | 'network' | 'cloud' | 'channel' | undefined>(
+    undefined,
+  );
 
   const [dataSource, setDataSource] = useState<any[]>([]);
 
@@ -41,6 +45,16 @@ const Detail = () => {
               response.result?.provider === 'gb28181-2016'
             ) {
               setType('media');
+            } else if (
+              response.result?.provider === 'Ctwing' ||
+              response.result?.provider === 'OneNet'
+            ) {
+              setType('cloud');
+            } else if (
+              response.result?.provider === 'modbus-tcp' ||
+              response.result?.provider === 'opc-ua'
+            ) {
+              setType('channel');
             } else {
               setType('network');
             }
@@ -80,6 +94,26 @@ const Detail = () => {
             }}
           />
         );
+      case 'cloud':
+        return (
+          <Cloud
+            data={data}
+            provider={provider}
+            change={() => {
+              setVisible(true);
+            }}
+          />
+        );
+      case 'channel':
+        return (
+          <Channel
+            data={data}
+            provider={provider}
+            change={() => {
+              setVisible(true);
+            }}
+          />
+        );
       default:
         return null;
     }
@@ -91,7 +125,7 @@ const Detail = () => {
         {visible ? (
           <Provider
             data={dataSource}
-            change={(param: any, typings: 'media' | 'network') => {
+            change={(param: any, typings: 'media' | 'network' | 'cloud' | 'channel') => {
               setType(typings);
               setProvider(param);
               setData({});

+ 26 - 0
src/pages/link/Channel/Opcua/index.less

@@ -0,0 +1,26 @@
+.topCard {
+  display: flex;
+  align-items: center;
+  .img {
+    position: relative;
+    top: 10px;
+    left: 20px;
+  }
+  .text {
+    margin-left: 24px;
+    .p1 {
+      height: 22px;
+      margin-bottom: 7px;
+      font-weight: 700;
+      font-size: 18px;
+      line-height: 22px;
+    }
+    .p2 {
+      height: 20px;
+      color: rgba(0, 0, 0, 0.75);
+      font-weight: 400;
+      font-size: 12px;
+      line-height: 20px;
+    }
+  }
+}

+ 241 - 0
src/pages/link/Channel/Opcua/index.tsx

@@ -0,0 +1,241 @@
+import { PageContainer } from '@ant-design/pro-layout';
+import ProTable, { ActionType, ProColumns } from '@jetlinks/pro-table';
+import { Card, Col, Row, Badge } from 'antd';
+import styles from './index.less';
+import { PermissionButton } from '@/components';
+import {
+  DeleteOutlined,
+  EditOutlined,
+  LinkOutlined,
+  PlayCircleOutlined,
+  PlusOutlined,
+  StopOutlined,
+} from '@ant-design/icons';
+import { useIntl } from 'umi';
+import { useRef } from 'react';
+import SearchComponent from '@/components/SearchComponent';
+
+const Opcua = () => {
+  const intl = useIntl();
+  const actionRef = useRef<ActionType>();
+  // const [param, setParam] = useState({});
+  const { permission } = PermissionButton.usePermission('link/Channel/Opcua');
+
+  const iconMap = new Map();
+  iconMap.set('1', require('/public/images/channel/1.png'));
+  iconMap.set('2', require('/public/images/channel/2.png'));
+  iconMap.set('3', require('/public/images/channel/3.png'));
+  iconMap.set('4', require('/public/images/channel/4.png'));
+  const background = require('/public/images/channel/background.png');
+
+  const columns: ProColumns<any>[] = [
+    {
+      title: '通道名称',
+      dataIndex: 'name',
+    },
+    {
+      title: 'IP',
+      dataIndex: 'ip',
+    },
+    {
+      title: '端口',
+      dataIndex: 'local',
+    },
+    {
+      title: '状态',
+      dataIndex: 'state',
+      renderText: (state) => (
+        <Badge text={state?.text} status={state?.value === 'disabled' ? 'error' : 'success'} />
+      ),
+    },
+    {
+      title: '操作',
+      valueType: 'option',
+      align: 'center',
+      width: 200,
+      render: (text, record) => [
+        <PermissionButton
+          isPermission={permission.update}
+          key="edit"
+          onClick={() => {
+            // setVisible(true);
+            // setCurrent(record);
+          }}
+          type={'link'}
+          style={{ padding: 0 }}
+          tooltip={{
+            title: intl.formatMessage({
+              id: 'pages.data.option.edit',
+              defaultMessage: '编辑',
+            }),
+          }}
+        >
+          <EditOutlined />
+        </PermissionButton>,
+        <PermissionButton
+          type="link"
+          key={'action'}
+          style={{ padding: 0 }}
+          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();
+            },
+          }}
+          isPermission={permission.action}
+          tooltip={{
+            title: intl.formatMessage({
+              id: `pages.data.option.${
+                record.state.value !== 'notActive' ? 'disabled' : 'enabled'
+              }`,
+              defaultMessage: record.state.value !== 'notActive' ? '禁用' : '启用',
+            }),
+          }}
+        >
+          {record.state.value !== 'notActive' ? <StopOutlined /> : <PlayCircleOutlined />}
+        </PermissionButton>,
+        <PermissionButton
+          isPermission={permission.view}
+          style={{ padding: 0 }}
+          key="button"
+          type="link"
+          tooltip={{
+            title: '设备接入',
+          }}
+          onClick={() => {}}
+        >
+          <LinkOutlined />
+        </PermissionButton>,
+        <PermissionButton
+          isPermission={permission.delete}
+          style={{ padding: 0 }}
+          popConfirm={{
+            title: '确认删除',
+            onConfirm: async () => {
+              // const resp: any = await service.remove(record.id);
+              // if (resp.status === 200) {
+              //   message.success(
+              //     intl.formatMessage({
+              //       id: 'pages.data.option.success',
+              //       defaultMessage: '操作成功!',
+              //     }),
+              //   );
+              //   actionRef.current?.reload();
+              // }
+            },
+          }}
+          key="button"
+          type="link"
+        >
+          <DeleteOutlined />
+        </PermissionButton>,
+      ],
+    },
+  ];
+
+  const topCard = [
+    {
+      numeber: '1',
+      title: 'OPC UA通道',
+      text: '配置OPC UA通道',
+    },
+    {
+      numeber: '2',
+      title: '设备接入网关',
+      text: '创建OPC UA设备接入网关',
+    },
+    {
+      numeber: '3',
+      title: '创建产品',
+      text: '创建产品,并选择接入方式为OPC UA',
+    },
+    {
+      numeber: '4',
+      title: '添加设备',
+      text: '添加设备,单独为每一个设备进行数据点绑定',
+    },
+  ];
+  return (
+    <PageContainer>
+      <Card style={{ marginBottom: 10 }}>
+        <Row gutter={[24, 24]}>
+          {topCard.map((item) => (
+            <Col span={6}>
+              <Card>
+                <div className={styles.topCard}>
+                  <div
+                    style={{
+                      background: `url(${background}) no-repeat`,
+                      backgroundSize: '100% 100%',
+                      width: '56px',
+                      height: '56px',
+                    }}
+                  >
+                    <img src={iconMap.get(item.numeber)} className={styles.img} />
+                  </div>
+                  <div className={styles.text}>
+                    <p className={styles.p1}>{item.title}</p>
+                    <p className={styles.p2}>{item.text}</p>
+                  </div>
+                </div>
+              </Card>
+            </Col>
+          ))}
+        </Row>
+      </Card>
+
+      <SearchComponent<any>
+        field={columns}
+        target="opcua"
+        onSearch={(data) => {
+          // 重置分页数据
+          actionRef.current?.reset?.();
+          console.log(data);
+          // setParam(data);
+        }}
+      />
+      <ProTable<UserItem>
+        actionRef={actionRef}
+        // params={param}
+        columns={columns}
+        search={false}
+        headerTitle={
+          <PermissionButton
+            onClick={() => {
+              // setMode('add');
+            }}
+            isPermission={permission.add}
+            key="button"
+            icon={<PlusOutlined />}
+            type="primary"
+          >
+            {intl.formatMessage({
+              id: 'pages.data.option.add',
+              defaultMessage: '新增',
+            })}
+          </PermissionButton>
+        }
+        // request={async (params) =>
+        //   service.query({ ...params, sorts: [{ name: 'createTime', order: 'desc' }] })
+        // }
+      />
+    </PageContainer>
+  );
+};
+export default Opcua;

+ 4 - 0
src/pages/link/Channel/Opcua/typings.d.ts

@@ -0,0 +1,4 @@
+type Item = {
+  id: string;
+  name: string;
+};

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

@@ -459,6 +459,7 @@ const Detail = observer(() => {
             },
           },
           webhook: {
+            'x-visible': id === 'webhook',
             type: 'void',
             properties: {
               url: {

+ 1 - 0
src/pages/notice/Template/Detail/index.tsx

@@ -1152,6 +1152,7 @@ const Detail = observer(() => {
           },
           webhook: {
             type: 'void',
+            'x-visible': id === 'webhook',
             properties: {
               contextAsBody: {
                 title: '请求体',

+ 22 - 16
src/pages/rule-engine/Alarm/Configuration/Save/index.tsx

@@ -196,6 +196,13 @@ const Save = (props: Props) => {
             message: '请选择关联触发场景',
           },
         ],
+        'x-component-props': {
+          placeholder: '请选择关联触发场景',
+          showSearch: true,
+          showArrow: true,
+          filterOption: (input: string, option: any) =>
+            option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0,
+        },
         'x-decorator-props': {
           gridSpan: 1,
           addonAfter: (
@@ -204,28 +211,27 @@ const Save = (props: Props) => {
               style={{ padding: 0 }}
               isPermission={true}
               onClick={() => {
-                // const tab: any = window.open(`${origin}/#/system/department?save=true`);
-                // tab!.onTabSaveSuccess = (value: any) => {
-                //   form.setFieldState('orgIdList', async (state) => {
-                // state.dataSource = await getOrg().then((resp) =>
-                //   resp.result?.map((item: Record<string, unknown>) => ({
-                //     ...item,
-                //     label: item.name,
-                //     value: item.id,
-                //   })),
-                // );
-                // state.value = [...(state.value || []), value.id];
-                // });
-                // };
+                const tab: any = window.open(`${origin}/#/iot/rule-engine/scene/Save`);
+                tab!.onTabSaveSuccess = (value: any) => {
+                  form.setFieldState('sceneId', async (state) => {
+                    state.dataSource = await getScene();
+                    // .then((resp) =>
+                    //   resp.result?.map((item: Record<string, unknown>) => ({
+                    //     ...item,
+                    //     label: item.name,
+                    //     value: item.id,
+                    //   })),
+                    // );
+                    console.log(value, 'value');
+                    state.value = value?.result?.id;
+                  });
+                };
               }}
             >
               <PlusOutlined />
             </PermissionButton>
           ),
         },
-        'x-component-props': {
-          placeholder: '请选择关联触发场景',
-        },
       },
       description: {
         title: '说明',

+ 1 - 1
src/pages/rule-engine/Alarm/Log/TabComponent/index.tsx

@@ -43,7 +43,7 @@ const TabComponent = observer((props: Props) => {
   const columns: ProColumns<any>[] = [
     {
       title: '名称',
-      dataIndex: 'name',
+      dataIndex: 'alarmName',
     },
     {
       title: '最近告警时间',

+ 9 - 0
src/pages/rule-engine/Scene/Save/index.tsx

@@ -109,6 +109,15 @@ export default () => {
     if (formData) {
       setLoading(true);
       const resp = formData.id ? await service.updateScene(formData) : await service.save(formData);
+
+      // 处理跳转新增
+      if ((window as any).onTabSaveSuccess) {
+        if (resp.result) {
+          (window as any).onTabSaveSuccess(resp);
+          setTimeout(() => window.close(), 300);
+        }
+      }
+
       setLoading(false);
       if (resp.status === 200) {
         message.success('操作成功');

+ 1 - 1
src/pages/rule-engine/Scene/Save/trigger/index.tsx

@@ -185,7 +185,7 @@ export default observer((props: TriggerProps) => {
                       { label: '固定设备', value: 'fixed' },
                       { label: '按部门', value: 'org' },
                     ]}
-                    fieldNames={{ label: 'name', value: 'id' }}
+                    // fieldNames={{ label: 'name', value: 'id' }}
                     style={{ width: 120 }}
                   />
                 </Form.Item>

+ 19 - 4
src/pages/rule-engine/Scene/index.tsx

@@ -174,6 +174,21 @@ const Scene = () => {
         defaultMessage: '触发方式',
       }),
       width: 120,
+      valueType: 'select',
+      valueEnum: {
+        manual: {
+          text: '手动触发',
+          status: 'manual',
+        },
+        timer: {
+          text: '定时触发',
+          status: 'timer',
+        },
+        device: {
+          text: '设备触发',
+          status: 'device',
+        },
+      },
       renderText: (record) => TriggerWayType[record],
       valueType: 'select',
       valueEnum: {
@@ -222,14 +237,14 @@ const Scene = () => {
           ''
         ),
       valueEnum: {
-        disable: {
-          text: '禁用',
-          status: 'offline',
-        },
         started: {
           text: '正常',
           status: 'started',
         },
+        disable: {
+          text: '禁用',
+          status: 'disable',
+        },
       },
     },
     {

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

@@ -40,7 +40,29 @@ const extraRouteObj = {
     children: [{ code: 'AMap', name: '地图' }],
   },
 };
-
+//额外路由
+export const extraRouteArr = [
+  {
+    code: 'account',
+    id: 'accout',
+    name: '个人中心',
+    url: '/account',
+    hideInMenu: true,
+    children: [
+      {
+        code: 'account/Center',
+        name: '基本设置',
+        url: '/account/center',
+      },
+      {
+        code: 'account/Center/bind',
+        name: '第三方页面',
+        url: '/account/center/bind',
+        hideInMenu: true,
+      },
+    ],
+  },
+];
 /**
  * 根据url获取映射的组件
  * @param files

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

@@ -35,6 +35,7 @@ export enum MENUS_CODE {
   'link/Certificate' = 'link/Certificate',
   'link/Gateway' = 'link/Gateway',
   'link/Opcua' = 'link/Opcua',
+  'link/Channal/Opcua' = 'link/Channal/Opcua',
   'link/Protocol/Debug' = 'link/Protocol/Debug',
   'link/Protocol' = 'link/Protocol',
   'link/Type' = 'link/Type',
@@ -109,6 +110,8 @@ export enum MENUS_CODE {
   'system/Menu/Detail' = 'system/Menu/Detail',
   'system/Department/Detail' = 'system/Department/Detail',
   'link/Type/Detail' = 'link/Type/Detail',
+  'account/Center' = 'account/Center',
+  'account/Center/bind' = 'account/Center/bind',
   'Northbound/DuerOS' = 'Northbound/DuerOS',
   'Northbound/AliCloud' = 'Northbound/AliCloud',
   'system/Platforms' = 'system/Platforms',