xieyonghong 3 lat temu
rodzic
commit
d15ee0bb6e
35 zmienionych plików z 1185 dodań i 479 usunięć
  1. 4 3
      src/components/FBraftEditor/index.tsx
  2. 1 1
      src/components/FTermArrayCards/index.tsx
  3. 1 1
      src/components/ProTableCard/CardItems/noticeConfig.tsx
  4. 18 6
      src/components/ProTableCard/CardItems/noticeTemplate.tsx
  5. 4 4
      src/pages/device/Instance/Detail/Config/index.tsx
  6. 36 11
      src/pages/device/Instance/Detail/Diagnose/Status/ManualInspection.tsx
  7. 61 101
      src/pages/device/Instance/Detail/Diagnose/Status/index.tsx
  8. 1 1
      src/pages/device/Instance/Detail/MetadataMap/EditableTable/index.tsx
  9. 65 26
      src/pages/device/Instance/Detail/MetadataMap/index.tsx
  10. 3 14
      src/pages/device/Instance/Detail/Reation/index.tsx
  11. 35 2
      src/pages/device/Instance/Detail/index.tsx
  12. 5 3
      src/pages/device/Instance/Export/index.tsx
  13. 6 5
      src/pages/device/Instance/Import/index.tsx
  14. 24 21
      src/pages/device/Product/Detail/Access/AccessConfig/index.tsx
  15. 15 16
      src/pages/device/Product/Detail/Access/index.tsx
  16. 4 3
      src/pages/device/Product/Detail/PropertyImport/index.tsx
  17. 14 7
      src/pages/device/components/Metadata/Base/Edit/index.tsx
  18. 29 40
      src/pages/link/AccessConfig/Detail/Access/index.tsx
  19. 32 25
      src/pages/link/Protocol/save/index.tsx
  20. 7 1
      src/pages/media/Cascade/Channel/index.tsx
  21. 111 0
      src/pages/notice/Config/BindUser/index.tsx
  22. 4 6
      src/pages/notice/Config/Debug/index.tsx
  23. 74 3
      src/pages/notice/Config/Detail/index.tsx
  24. 30 4
      src/pages/notice/Config/Log/index.tsx
  25. 87 67
      src/pages/notice/Config/SyncUser/index.tsx
  26. 46 0
      src/pages/notice/Config/service.ts
  27. 192 51
      src/pages/notice/Template/Detail/index.tsx
  28. 8 2
      src/pages/notice/Template/Log/index.tsx
  29. 1 1
      src/pages/notice/Template/typings.d.ts
  30. 5 2
      src/pages/rule-engine/Alarm/Log/SolveLog/index.tsx
  31. 2 1
      src/pages/rule-engine/Alarm/Log/TabComponent/index.tsx
  32. 91 4
      src/pages/rule-engine/Scene/Save/index.tsx
  33. 136 24
      src/pages/rule-engine/Scene/TriggerTerm/index.tsx
  34. 6 0
      src/pages/rule-engine/Scene/service.ts
  35. 27 23
      src/pages/system/Relationship/Save/index.tsx

+ 4 - 3
src/components/FBraftEditor/index.tsx

@@ -15,19 +15,20 @@ const FBraftEditor = connect((props: Props) => {
   );
 
   return (
-    <>
+    <div>
       {
         // @ts-ignore
         <BraftEditor
+          style={{ height: 350 }}
           placeholder={props.placeholder}
           value={editorState}
           onChange={(state) => {
             setEditorState(state);
-            props.onChange(state.toHTML());
+            props.onChange(state.toHTML() === '<p></p>' ? undefined : state.toHTML());
           }}
         />
       }
-    </>
+    </div>
   );
 }, mapProps());
 export default FBraftEditor;

+ 1 - 1
src/components/FTermArrayCards/index.tsx

@@ -123,7 +123,7 @@ export const FTermArrayCards: ComposedArrayCards = observer((props) => {
   };
 
   const renderAddition = () => {
-    return schema.reduceProperties((addition, schema3, key) => {
+    return schema.reduceProperties((addition: any, schema3: any, key: any) => {
       if (isAdditionComponent(schema3)) {
         return <RecursionField schema={schema3} name={key} />;
       }

+ 1 - 1
src/components/ProTableCard/CardItems/noticeConfig.tsx

@@ -15,7 +15,7 @@ export default (props: NoticeCardProps) => {
     <TableCard detail={props.detail} actions={props.actions} showStatus={false} showMask={false}>
       <div className={'pro-table-card-item'}>
         <div className={'card-item-avatar'}>
-          <img width={88} height={88} src={imgMap[props.type]} alt={props.type} />
+          <img width={88} height={88} src={imgMap[props.type][props.provider]} alt={props.type} />
         </div>
         <div className={'card-item-body'}>
           <div className={'card-item-header'}>

+ 18 - 6
src/components/ProTableCard/CardItems/noticeTemplate.tsx

@@ -10,11 +10,23 @@ export interface NoticeCardProps extends TemplateItem {
 }
 
 export const imgMap = {
-  dingTalk: require('/public/images/notice/dingtalk.png'),
-  weixin: require('/public/images/notice/wechat.png'),
-  email: require('/public/images/notice/email.png'),
-  voice: require('/public/images/notice/voice.png'),
-  sms: require('/public/images/notice/sms.png'),
+  dingTalk: {
+    dingTalkMessage: require('/public/images/notice/dingtalk.png'),
+    dingTalkRobotWebHook: require('/public/images/notice/dingTalk-rebot.png'),
+  },
+  weixin: {
+    corpMessage: require('/public/images/notice/wechat.png'),
+    officialMessage: require('/public/images/notice/weixin-official.png'),
+  },
+  email: {
+    embedded: require('/public/images/notice/email.png'),
+  },
+  voice: {
+    aliyun: require('/public/images/notice/voice.png'),
+  },
+  sms: {
+    aliyunSms: require('/public/images/notice/sms.png'),
+  },
 };
 
 export const typeList = {
@@ -42,7 +54,7 @@ export default (props: NoticeCardProps) => {
     <TableCard actions={props.actions} showStatus={false} detail={props.detail} showMask={false}>
       <div className={'pro-table-card-item'}>
         <div className={'card-item-avatar'}>
-          <img width={88} height={88} src={imgMap[props.type]} alt={props.type} />
+          <img width={88} height={88} src={imgMap[props.type][props.provider]} alt={props.type} />
         </div>
         <div className={'card-item-body'}>
           <div className={'card-item-header'}>

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

@@ -7,7 +7,7 @@ import {
   CheckOutlined,
   EditOutlined,
   QuestionCircleOutlined,
-  UndoOutlined,
+  SyncOutlined,
 } from '@ant-design/icons';
 import Edit from './Edit';
 import { PermissionButton } from '@/components';
@@ -66,14 +66,14 @@ const Config = () => {
       if (isExit(item.property)) {
         return (
           <div>
-            <span style={{ marginRight: '10px' }}>{config[item.property]}</span>
+            <span style={{ marginRight: '10px' }}>{config[item.property] || '--'}</span>
             <Tooltip title={`有效值:${config[item.property]}`}>
               <QuestionCircleOutlined />
             </Tooltip>
           </div>
         );
       } else {
-        return <div>{config[item.property]}</div>;
+        return <div>{config[item.property] || '--'}</div>;
       }
     } else {
       return '--';
@@ -132,7 +132,7 @@ const Config = () => {
               type="link"
               isPermission={permission.update}
             >
-              <UndoOutlined />
+              <SyncOutlined />
               恢复默认
               <Tooltip
                 title={`该设备单独编辑过配置信息,点击此将恢复成默认的配置信息,请谨慎操作。`}

+ 36 - 11
src/pages/device/Instance/Detail/Diagnose/Status/ManualInspection.tsx

@@ -3,6 +3,8 @@ import { createSchemaField } from '@formily/react';
 import type { ISchema } from '@formily/json-schema';
 import { Form, FormGrid, FormItem, Input, Password, PreviewText } from '@formily/antd';
 import { Modal } from 'antd';
+import styles from './index.less';
+import { ExclamationCircleFilled } from '@ant-design/icons';
 
 const componentMap = {
   string: 'Input',
@@ -11,12 +13,12 @@ const componentMap = {
 
 interface Props {
   close: () => void;
-  metadata: any;
+  data: any;
   ok: (data: any) => void;
 }
 
 const ManualInspection = (props: Props) => {
-  const { metadata } = props;
+  const { data } = props;
 
   const form = createForm({
     validateFirst: true,
@@ -33,9 +35,9 @@ const ManualInspection = (props: Props) => {
     },
   });
 
-  const configToSchema = (data: any[]) => {
+  const configToSchema = (list: any[]) => {
     const config = {};
-    data.forEach((item) => {
+    list.forEach((item) => {
       config[item.property] = {
         type: 'string',
         title: item.name,
@@ -47,6 +49,7 @@ const ManualInspection = (props: Props) => {
         },
         'x-component-props': {
           value: '',
+          placeholder: `请输入${item.name}`,
         },
       };
     });
@@ -64,7 +67,7 @@ const ManualInspection = (props: Props) => {
             minColumns: [1],
             maxColumns: [1],
           },
-          properties: configToSchema(metadata?.data?.properties),
+          properties: configToSchema(data?.data?.properties),
         },
       },
     };
@@ -79,42 +82,64 @@ const ManualInspection = (props: Props) => {
       </>
     );
   };
+  const renderComponent = () => (
+    <div style={{ backgroundColor: '#f6f6f6', padding: 10 }}>
+      {(data?.data?.properties || []).map((item: any) => (
+        <div key={item.property}>
+          <span>{item.name}</span>:{' '}
+          <span>{item.type.type !== 'password' ? data?.check[item.property] : '******'}</span>
+        </div>
+      ))}
+    </div>
+  );
+
   return (
     <Modal
       title="人工检查"
       onCancel={() => {
         props.close();
       }}
+      width={600}
       onOk={async () => {
         const values = (await form.submit()) as any;
-        if (metadata?.check) {
+        if (!data?.check) {
           props.ok({
             status: 'error',
-            data: metadata,
+            data: data,
           });
         } else {
           let flag = true;
           Object.keys(values).forEach((key) => {
-            if (values[key] !== metadata?.check[key]) {
+            if (values[key] !== data?.check[key]) {
               flag = false;
             }
           });
           if (flag) {
             props.ok({
               status: 'success',
-              data: metadata,
+              data: data,
             });
           } else {
             props.ok({
               status: 'error',
-              data: metadata,
+              data: data,
             });
           }
         }
       }}
       visible
     >
-      {renderConfigCard()}
+      <div className={styles.alert}>
+        <ExclamationCircleFilled style={{ marginRight: 10 }} />
+        {data.type === 'product'
+          ? `当前填写的数据将与产品-设备接入配置中的${data.data.name}数据进行比对`
+          : `当前填写的数据将与设备-实例信息配置中的${data.data.name}数据进行比对`}
+      </div>
+      <div style={{ marginTop: 10 }}>
+        已配置参数
+        {renderComponent()}
+      </div>
+      <div style={{ marginTop: 10 }}>{renderConfigCard()}</div>
     </Modal>
   );
 };

+ 61 - 101
src/pages/device/Instance/Detail/Diagnose/Status/index.tsx

@@ -74,6 +74,7 @@ const Status = observer((props: Props) => {
   const productPermission = PermissionButton.usePermission('device/Product').permission;
   const networkPermission = PermissionButton.usePermission('link/Type').permission;
   const devicePermission = PermissionButton.usePermission('device/Instance').permission;
+
   const [diagnoseVisible, setDiagnoseVisible] = useState<boolean>(false);
   const [artificialVisible, setArtificialVisible] = useState<boolean>(false);
   const [diagnoseData, setDiagnoseData] = useState<any>({});
@@ -306,7 +307,7 @@ const Status = observer((props: Props) => {
       } else {
         data = {
           status: proItem?.state === 1 ? 'success' : 'error',
-          text: proItem?.state === 1 ? '异常' : '正常',
+          text: proItem?.state === 1 ? '正常' : '异常',
           info:
             proItem?.state === 1 ? null : (
               <div className={styles.infoItem}>
@@ -440,8 +441,9 @@ const Status = observer((props: Props) => {
                               onClick={() => {
                                 setArtificialVisible(true);
                                 setArtificiaData({
+                                  type: 'product',
                                   data: item,
-                                  name: `productAuth${index}`,
+                                  key: `productAuth${index}`,
                                   check: proItem.configuration,
                                 });
                               }}
@@ -500,8 +502,9 @@ const Status = observer((props: Props) => {
                               onClick={() => {
                                 setArtificialVisible(true);
                                 setArtificiaData({
+                                  type: 'device',
                                   data: item,
-                                  name: `deviceAuth${index}`,
+                                  key: `deviceAuth${index}`,
                                   check: InstanceModel.detail?.configuration,
                                 });
                               }}
@@ -674,15 +677,17 @@ const Status = observer((props: Props) => {
   };
 
   useEffect(() => {
-    if (!props.flag) {
-      handleSearch();
-    } else {
-      const dt = Store.get('diagnose-status');
-      DiagnoseStatusModel.status = dt?.status;
-      DiagnoseStatusModel.list = dt?.list || [];
-      props.onChange('success');
+    if (devicePermission.view) {
+      if (!props.flag) {
+        handleSearch();
+      } else {
+        const dt = Store.get('diagnose-status');
+        DiagnoseStatusModel.status = dt?.status;
+        DiagnoseStatusModel.list = dt?.list || [];
+        props.onChange('success');
+      }
     }
-  }, []);
+  }, [devicePermission]);
 
   return (
     <Row gutter={24}>
@@ -745,107 +750,62 @@ const Status = observer((props: Props) => {
       )}
       {artificialVisible && (
         <ManualInspection
-          metadata={artificiaData}
+          data={artificiaData}
           close={() => {
             setArtificialVisible(false);
           }}
           ok={(params: any) => {
             setArtificialVisible(false);
-            console.log(params);
             if (params.status === 'success') {
-              DiagnoseStatusModel.status[params.data.name] = {
+              DiagnoseStatusModel.status[params.data.key] = {
                 status: 'success',
                 text: '正常',
                 info: null,
               };
             } else {
-              if (!params.data.name.includes('device')) {
-                DiagnoseStatusModel.status[params.data.name] = {
-                  status: 'error',
-                  text: '异常',
-                  info: (
-                    <div className={styles.infoItem}>
-                      <Badge
-                        status="default"
-                        text={
-                          <span>
-                            产品-{params.data.name}配置错误,请
-                            <a
-                              onClick={() => {
-                                const url = getMenuPathByParams(
-                                  MENUS_CODE['device/Product/Detail'],
-                                  InstanceModel.detail?.productId,
-                                );
-                                const tab: any = window.open(`${origin}/#${url}?key=access`);
-                                tab!.onTabSaveSuccess = (value: any) => {
-                                  if (value) {
-                                    diagnoseConfig();
-                                  }
-                                };
-                              }}
-                            >
-                              重新配置
-                            </a>
-                            或
-                            <a
-                              onClick={() => {
-                                setArtificialVisible(true);
-                                setArtificiaData(params.data);
-                              }}
-                            >
-                              重新比对
-                            </a>
-                            。
-                          </span>
-                        }
-                      />
-                    </div>
-                  ),
-                };
-              } else {
-                DiagnoseStatusModel.status[params.data.name] = {
-                  status: 'error',
-                  text: '异常',
-                  info: (
-                    <div className={styles.infoItem}>
-                      <Badge
-                        status="default"
-                        text={
-                          <span>
-                            设备-{params.data.name}配置错误,请
-                            <a
-                              onClick={() => {
-                                const url = getMenuPathByParams(
-                                  MENUS_CODE['device/Product/Detail'],
-                                  InstanceModel.detail?.productId,
-                                );
-                                const tab: any = window.open(`${origin}/#${url}?key=access`);
-                                tab!.onTabSaveSuccess = (value: any) => {
-                                  if (value) {
-                                    diagnoseConfig();
-                                  }
-                                };
-                              }}
-                            >
-                              重新配置
-                            </a>
-                            或
-                            <a
-                              onClick={() => {
-                                setArtificialVisible(true);
-                                setArtificiaData(params.data);
-                              }}
-                            >
-                              重新比对
-                            </a>
-                            。
-                          </span>
-                        }
-                      />
-                    </div>
-                  ),
-                };
-              }
+              DiagnoseStatusModel.status[params.data.key] = {
+                status: 'error',
+                text: '异常',
+                info: (
+                  <div className={styles.infoItem}>
+                    <Badge
+                      status="default"
+                      text={
+                        <span>
+                          {params.data.type === 'device' ? '设备' : '产品'}-{params.data.data.name}
+                          配置错误,请
+                          <a
+                            onClick={() => {
+                              const url = getMenuPathByParams(
+                                MENUS_CODE['device/Product/Detail'],
+                                InstanceModel.detail?.productId,
+                              );
+                              const tab: any = window.open(`${origin}/#${url}?key=access`);
+                              tab!.onTabSaveSuccess = (value: any) => {
+                                if (value) {
+                                  diagnoseConfig();
+                                }
+                              };
+                            }}
+                          >
+                            重新配置
+                          </a>
+                          或
+                          <a
+                            onClick={() => {
+                              setArtificialVisible(true);
+                              setArtificiaData(params.data);
+                            }}
+                          >
+                            重新比对
+                          </a>
+                          。
+                        </span>
+                      }
+                    />
+                  </div>
+                ),
+              };
             }
           }}
         />

+ 1 - 1
src/pages/device/Instance/Detail/MetadataMap/EditableTable/index.tsx

@@ -74,7 +74,7 @@ const EditableCell = ({
             (option?.children || '').toLowerCase()?.indexOf(input.toLowerCase()) >= 0
           }
         >
-          <Select.Option value={record.metadataId}>使用原始属性</Select.Option>
+          <Select.Option value={record.metadataId}>使用物模型属性</Select.Option>
           {record.originalId !== record.metadataId && (
             <Select.Option value={record.originalId}>
               {temp?.name}({temp?.id})

+ 65 - 26
src/pages/device/Instance/Detail/MetadataMap/index.tsx

@@ -5,6 +5,8 @@ import EditableTable from './EditableTable';
 import { getMenuPathByParams, MENUS_CODE } from '@/utils/menu';
 import type { ProductItem } from '@/pages/device/Product/typings';
 import { useParams } from 'umi';
+import { PermissionButton } from '@/components';
+
 interface Props {
   type: 'device' | 'product';
 }
@@ -14,6 +16,7 @@ const MetadataMap = (props: Props) => {
   const [product, setProduct] = useState<Partial<ProductItem>>();
   const [data, setData] = useState<any>({});
   const params = useParams<{ id: string }>();
+  const { permission } = PermissionButton.usePermission('device/Product');
 
   const handleSearch = async () => {
     if (props.type === 'product') {
@@ -45,33 +48,22 @@ const MetadataMap = (props: Props) => {
   };
 
   const renderComponent = () => {
+    const metadata = JSON.parse(product?.metadata || '{}');
+    const dmetadata = JSON.parse(data?.metadata || '{}');
     if (product) {
-      if (!product.accessId) {
-        return (
-          <Empty
-            description={
-              <span>
-                请配置对应产品的
-                <a
-                  onClick={() => {
-                    checkUrl('access');
-                  }}
-                >
-                  设备接入方式
-                </a>
-              </span>
-            }
-          />
-        );
-      } else {
-        const metadata = JSON.parse(product?.metadata || '{}');
-        const dmetadata = JSON.parse(data?.metadata || '{}');
-        if (
-          (type === 'device' &&
-            (metadata?.properties || []).length === 0 &&
-            (dmetadata?.properties || []).length === 0) ||
-          (type === 'product' && (dmetadata?.properties || []).length === 0)
-        ) {
+      const flag =
+        (type === 'device' &&
+          (metadata?.properties || []).length === 0 &&
+          (dmetadata?.properties || []).length === 0) ||
+        (type === 'product' && (dmetadata?.properties || []).length === 0);
+      if (!product.accessId && flag) {
+        if (!permission.update) {
+          return (
+            <Empty
+              description={<span>请联系管理员配置物模型属性,并选择对应产品的设备接入方式</span>}
+            />
+          );
+        } else {
           return (
             <Empty
               description={
@@ -84,11 +76,58 @@ const MetadataMap = (props: Props) => {
                   >
                     物模型属性
                   </a>
+                  ,并选择对应产品的
+                  <a
+                    onClick={() => {
+                      checkUrl('access');
+                    }}
+                  >
+                    设备接入方式
+                  </a>
                 </span>
               }
             />
           );
         }
+      } else if (flag && product.accessId) {
+        return (
+          <Empty
+            description={
+              !permission.update ? (
+                <span>请联系管理员配置物模型属性</span>
+              ) : (
+                <span>
+                  请配置对应产品的
+                  <a
+                    onClick={() => {
+                      checkUrl('metadata');
+                    }}
+                  >
+                    物模型属性
+                  </a>
+                </span>
+              )
+            }
+          />
+        );
+      } else if (!flag && !product.accessId) {
+        return (
+          <Empty
+            description={
+              <span>
+                请选择对应产品的
+                <a
+                  onClick={() => {
+                    checkUrl('access');
+                  }}
+                >
+                  设备接入方式
+                </a>
+              </span>
+            }
+          />
+        );
+      } else {
         return <EditableTable data={data} type={type} />;
       }
     }

+ 3 - 14
src/pages/device/Instance/Detail/Reation/index.tsx

@@ -1,7 +1,7 @@
 import { Descriptions, Tooltip } from 'antd';
 import { InstanceModel, service } from '@/pages/device/Instance';
 import { useEffect, useState } from 'react';
-import { history, useParams } from 'umi';
+import { useParams } from 'umi';
 import { EditOutlined, QuestionCircleOutlined } from '@ant-design/icons';
 import Edit from './Edit';
 import { PermissionButton } from '@/components';
@@ -9,17 +9,6 @@ import _ from 'lodash';
 
 const Reation = () => {
   const params = useParams<{ id: string }>();
-  useEffect(() => {
-    const id = InstanceModel.current?.id || params.id;
-    if (id) {
-      service.getConfigMetadata(id).then((response) => {
-        InstanceModel.config = response?.result;
-      });
-    } else {
-      history.goBack();
-    }
-  }, []);
-
   const [data, setData] = useState<any[]>([]);
   const [visible, setVisible] = useState<boolean>(false);
   const { permission } = PermissionButton.usePermission('device/Instance');
@@ -38,7 +27,7 @@ const Reation = () => {
     if (id) {
       setData(InstanceModel.detail?.relations || []);
     }
-  }, [id]);
+  }, [InstanceModel.detail?.relations]);
 
   return (
     <div style={{ width: '100%', marginTop: '20px' }}>
@@ -68,7 +57,7 @@ const Reation = () => {
       >
         {(data || [])?.map((item: any) => (
           <Descriptions.Item span={1} label={item.relationName} key={item.objectId}>
-            {_.map(item?.related || [], 'name').join(',')}
+            {item?.related ? _.map(item?.related || [], 'name').join(',') : '--'}
           </Descriptions.Item>
         ))}
       </Descriptions>

+ 35 - 2
src/pages/device/Instance/Detail/index.tsx

@@ -1,7 +1,7 @@
 import { PageContainer } from '@ant-design/pro-layout';
 import { InstanceModel } from '@/pages/device/Instance';
 import { history, useParams } from 'umi';
-import { Badge, Card, Descriptions, Divider, Tooltip } from 'antd';
+import { Badge, Card, Descriptions, Divider, message, Space, Tooltip } from 'antd';
 import type { ReactNode } from 'react';
 import { useEffect, useState } from 'react';
 import { observer } from '@formily/react';
@@ -35,6 +35,7 @@ const InstanceDetail = observer(() => {
   const [tab, setTab] = useState<string>('detail');
   const params = useParams<{ id: string }>();
   const service = new Service('device-instance');
+  const { permission } = PermissionButton.usePermission('device/Instance');
 
   // const resetMetadata = async () => {
   //   const resp = await service.deleteMetadata(params.id);
@@ -260,7 +261,39 @@ const InstanceDetail = observer(() => {
         <>
           {InstanceModel.detail?.name}
           <Divider type="vertical" />
-          {deviceStatus.get(InstanceModel.detail?.state?.value)}
+          <Space>
+            {deviceStatus.get(InstanceModel.detail?.state?.value)}
+            <PermissionButton
+              type={'link'}
+              key={'state'}
+              popConfirm={{
+                title:
+                  InstanceModel.detail?.state?.value !== 'notActive'
+                    ? '确认断开连接'
+                    : '确认启用设备',
+                onConfirm: async () => {
+                  if (InstanceModel.detail?.state?.value !== 'notActive') {
+                    await service.undeployDevice(params.id);
+                  } else {
+                    await service.deployDevice(params.id);
+                  }
+                  message.success(
+                    intl.formatMessage({
+                      id: 'pages.data.option.success',
+                      defaultMessage: '操作成功!',
+                    }),
+                  );
+                  getDetail(params.id);
+                },
+              }}
+              isPermission={permission.action}
+              tooltip={{
+                title: InstanceModel.detail?.state?.value !== 'notActive' ? '断开连接' : '启用设备',
+              }}
+            >
+              {InstanceModel.detail?.state?.value !== 'notActive' ? '断开连接' : '启用设备'}
+            </PermissionButton>
+          </Space>
         </>
       }
       // extra={[

+ 5 - 3
src/pages/device/Instance/Export/index.tsx

@@ -49,9 +49,10 @@ const Export = (props: Props) => {
         type: 'void',
         'x-component': 'FormLayout',
         'x-component-props': {
-          labelCol: 4,
-          wrapperCol: 18,
-          labelAlign: 'right',
+          // labelCol: 4,
+          // wrapperCol: 18,
+          // labelAlign: 'right',
+          layout: 'vertical',
         },
         properties: {
           product: {
@@ -103,6 +104,7 @@ const Export = (props: Props) => {
     } else {
       downloadFile(`/${SystemConst.API_BASE}/device/instance/export.${values.fileType}`, params);
     }
+    close();
   };
   return (
     <Modal

+ 6 - 5
src/pages/device/Instance/Import/index.tsx

@@ -88,7 +88,7 @@ const NormalUpload = (props: any) => {
           dt += temp;
           setCount(dt);
         } else {
-          setErrMessage(res.message);
+          setErrMessage(res.message || '失败');
         }
       };
       source.onerror = () => {
@@ -221,9 +221,10 @@ const Import = (props: Props) => {
         type: 'void',
         'x-component': 'FormLayout',
         'x-component-props': {
-          labelCol: 4,
-          wrapperCol: 18,
-          labelAlign: 'right',
+          // labelCol: 6,
+          // wrapperCol: 18,
+          // labelAlign: 'right',
+          layout: 'vertical',
         },
         properties: {
           product: {
@@ -264,7 +265,7 @@ const Import = (props: Props) => {
       visible={visible}
       onCancel={() => close()}
       width="35vw"
-      title="导"
+      title="导"
       onOk={() => close()}
       footer={[
         <Button key="cancel" onClick={() => close()}>

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

@@ -83,29 +83,32 @@ const AccessConfig = (props: Props) => {
       visible
       width={1200}
       title={'设备接入配置'}
-      onOk={() => {
+      onOk={async () => {
         if (!!currrent) {
-          service1
-            .update({
-              ...productModel.current,
-              transportProtocol: currrent.transport,
-              protocolName: currrent.protocolDetail.name,
-              accessId: currrent.id,
-              accessName: currrent.name,
-              accessProvider: currrent.provider,
-              messageProtocol: currrent.protocol,
-            })
-            .then((resp) => {
-              if (resp.status === 200) {
-                service1.detail(productModel.current?.id || '').then((res) => {
-                  if (res.status === 200) {
-                    productModel.current = { ...res.result };
-                    message.success('操作成功!');
+          const resp: any = await service1.update({
+            ...productModel.current,
+            transportProtocol: currrent.transport,
+            protocolName: currrent.protocolDetail.name,
+            accessId: currrent.id,
+            accessName: currrent.name,
+            accessProvider: currrent.provider,
+            messageProtocol: currrent.protocol,
+          });
+          if (resp.status === 200) {
+            service1
+              .changeDeploy(productModel.current?.id || '', 'deploy')
+              .subscribe((response) => {
+                if (response) {
+                  service1.detail(productModel.current?.id || '').then((res) => {
+                    if (res.status === 200) {
+                      productModel.current = { ...res.result };
+                      message.success('操作成功!');
+                    }
                     close();
-                  }
-                });
-              }
-            });
+                  });
+                }
+              });
+          }
         } else {
           message.success('请选择接入方式');
         }

+ 15 - 16
src/pages/device/Product/Detail/Access/index.tsx

@@ -89,11 +89,11 @@ const Access = () => {
       title: 'topic',
       dataIndex: 'topic',
       key: 'topic',
-      ellipsis: true,
-      align: 'center',
       render: (text: any) => (
         <Tooltip placement="topLeft" title={text}>
-          {text}
+          <div style={{ overflow: 'hidden', whiteSpace: 'nowrap', textOverflow: 'ellipsis' }}>
+            {text}
+          </div>
         </Tooltip>
       ),
     },
@@ -119,11 +119,11 @@ const Access = () => {
       title: '说明',
       dataIndex: 'description',
       key: 'description',
-      ellipsis: true,
-      align: 'center',
       render: (text: any) => (
         <Tooltip placement="topLeft" title={text}>
-          {text}
+          <div style={{ overflow: 'hidden', whiteSpace: 'nowrap', textOverflow: 'ellipsis' }}>
+            {text}
+          </div>
         </Tooltip>
       ),
     },
@@ -136,7 +136,6 @@ const Access = () => {
       key: 'group',
       ellipsis: true,
       width: 100,
-      align: 'center',
       onCell: (record: any, index: number) => {
         const list = (config?.routes || []).sort((a: any, b: any) => a - b) || [];
         const arr = list.filter((res: any) => {
@@ -154,11 +153,11 @@ const Access = () => {
       title: '地址',
       dataIndex: 'address',
       key: 'address',
-      ellipsis: true,
-      align: 'center',
       render: (text: any) => (
         <Tooltip placement="topLeft" title={text}>
-          {text}
+          <div style={{ overflow: 'hidden', whiteSpace: 'nowrap', textOverflow: 'ellipsis' }}>
+            {text}
+          </div>
         </Tooltip>
       ),
     },
@@ -166,11 +165,11 @@ const Access = () => {
       title: '示例',
       dataIndex: 'example',
       key: 'example',
-      ellipsis: true,
-      align: 'center',
       render: (text: any) => (
         <Tooltip placement="topLeft" title={text}>
-          {text}
+          <div style={{ overflow: 'hidden', whiteSpace: 'nowrap', textOverflow: 'ellipsis' }}>
+            {text}
+          </div>
         </Tooltip>
       ),
     },
@@ -178,11 +177,11 @@ const Access = () => {
       title: '说明',
       dataIndex: 'description',
       key: 'description',
-      ellipsis: true,
-      align: 'center',
       render: (text: any) => (
         <Tooltip placement="topLeft" title={text}>
-          {text}
+          <div style={{ overflow: 'hidden', whiteSpace: 'nowrap', textOverflow: 'ellipsis' }}>
+            {text}
+          </div>
         </Tooltip>
       ),
     },

+ 4 - 3
src/pages/device/Product/Detail/PropertyImport/index.tsx

@@ -138,9 +138,10 @@ const PropertyImport = (props: Props) => {
         type: 'void',
         'x-component': 'FormLayout',
         'x-component-props': {
-          labelCol: 4,
-          wrapperCol: 18,
-          labelAlign: 'right',
+          // labelCol: 4,
+          // wrapperCol: 18,
+          // labelAlign: 'right',
+          layout: 'vertical',
         },
         properties: {
           fileType: {

+ 14 - 7
src/pages/device/components/Metadata/Base/Edit/index.tsx

@@ -26,7 +26,7 @@ import {
   FileTypeList,
   PropertySource,
 } from '@/pages/device/data';
-import { useMemo } from 'react';
+import { useMemo, useState } from 'react';
 import { productModel } from '@/pages/device/Product';
 import { service } from '@/pages/device/components/Metadata';
 import { Store } from 'jetlinks-store';
@@ -53,16 +53,14 @@ interface Props {
 
 const Edit = observer((props: Props) => {
   const intl = useIntl();
+  const [loading, setLoading] = useState<boolean>(false);
   const form = useMemo(
     () =>
       createForm({
         initialValues: MetadataModel.item as Record<string, unknown>,
         effects: () => {
           onFieldReact('expands.metrics.*.*', (field, form1) => {
-            console.log('指标配置');
             const type = field.query('valueType.type').take() as Field;
-            console.log(type.value, 'value');
-
             const componentMap = {
               int: 'NumberPicker',
               long: 'NumberPicker',
@@ -458,6 +456,7 @@ const Edit = observer((props: Props) => {
             'x-decorator': 'FormItem',
             'x-component': 'Select',
             enum: PropertySource,
+            'x-visible': props.type === 'product',
           },
           'virtualRule.type': {
             type: 'string',
@@ -975,6 +974,7 @@ const Edit = observer((props: Props) => {
   const { type } = MetadataModel;
 
   const saveMetadata = async (deploy?: boolean) => {
+    setLoading(true);
     const params = (await form.submit()) as MetadataItem;
 
     if (!typeMap.get(props.type)) return;
@@ -1016,7 +1016,6 @@ const Edit = observer((props: Props) => {
     // const result = await saveMap.get(props.type);
     const result = await asyncUpdateMedata(props.type, _data);
     if (result.status === 200) {
-      message.success('操作成功!');
       if ((window as any).onTabSaveSuccess) {
         if (result) {
           (window as any).onTabSaveSuccess(result);
@@ -1026,6 +1025,8 @@ const Edit = observer((props: Props) => {
         Store.set(SystemConst.REFRESH_METADATA_TABLE, true);
         if (deploy) {
           Store.set('product-deploy', deploy);
+        } else {
+          message.success('操作成功!');
         }
         MetadataModel.edit = false;
         MetadataModel.item = {};
@@ -1033,6 +1034,7 @@ const Edit = observer((props: Props) => {
     } else {
       message.error('操作失败!');
     }
+    setLoading(false);
   };
 
   const menu = (
@@ -1064,11 +1066,16 @@ const Edit = observer((props: Props) => {
         placement={'right'}
         extra={
           props.type === 'product' ? (
-            <Dropdown.Button type="primary" onClick={() => saveMetadata()} overlay={menu}>
+            <Dropdown.Button
+              loading={loading}
+              type="primary"
+              onClick={() => saveMetadata()}
+              overlay={menu}
+            >
               保存数据
             </Dropdown.Button>
           ) : (
-            <Button type="primary" onClick={() => saveMetadata()}>
+            <Button loading={loading} type="primary" onClick={() => saveMetadata()}>
               保存数据
             </Button>
           )

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

@@ -152,12 +152,7 @@ const Access = (props: Props) => {
       key: 'group',
       ellipsis: true,
       align: 'center',
-      width: 80,
-      render: (text: any) => (
-        <Tooltip placement="top" title={text}>
-          {text}
-        </Tooltip>
-      ),
+      width: 100,
       onCell: (record: any, index: number) => {
         const list = (config?.routes || []).sort((a: any, b: any) => a - b) || [];
         const arr = list.filter((res: any) => {
@@ -175,12 +170,11 @@ const Access = (props: Props) => {
       title: 'topic',
       dataIndex: 'topic',
       key: 'topic',
-      ellipsis: true,
-      align: 'center',
-      with: '30%',
       render: (text: any) => (
-        <Tooltip placement="top" title={text}>
-          {text}
+        <Tooltip placement="topLeft" title={text}>
+          <div style={{ overflow: 'hidden', whiteSpace: 'nowrap', textOverflow: 'ellipsis' }}>
+            {text}
+          </div>
         </Tooltip>
       ),
     },
@@ -189,8 +183,8 @@ const Access = (props: Props) => {
       dataIndex: 'stream',
       key: 'stream',
       ellipsis: true,
-      width: 80,
       align: 'center',
+      width: 100,
       render: (text: any, record: any) => {
         const list = [];
         if (record?.upstream) {
@@ -206,11 +200,11 @@ const Access = (props: Props) => {
       title: '说明',
       dataIndex: 'description',
       key: 'description',
-      ellipsis: true,
-      align: 'center',
       render: (text: any) => (
-        <Tooltip placement="top" title={text}>
-          {text}
+        <Tooltip placement="topLeft" title={text}>
+          <div style={{ overflow: 'hidden', whiteSpace: 'nowrap', textOverflow: 'ellipsis' }}>
+            {text}
+          </div>
         </Tooltip>
       ),
     },
@@ -218,16 +212,11 @@ const Access = (props: Props) => {
 
   const columnsHTTP: any[] = [
     {
-      title: '地址',
-      dataIndex: 'address',
-      key: 'address',
+      title: '分组',
+      dataIndex: 'group',
+      key: 'group',
       ellipsis: true,
-      align: 'center',
-      render: (text: any) => (
-        <Tooltip placement="top" title={text}>
-          {text}
-        </Tooltip>
-      ),
+      width: 100,
       onCell: (record: any, index: number) => {
         const list = (config?.routes || []).sort((a: any, b: any) => a - b) || [];
         const arr = list.filter((res: any) => {
@@ -242,14 +231,14 @@ const Access = (props: Props) => {
       },
     },
     {
-      title: '分组',
-      dataIndex: 'group',
-      key: 'group',
-      ellipsis: true,
-      align: 'center',
+      title: '地址',
+      dataIndex: 'address',
+      key: 'address',
       render: (text: any) => (
-        <Tooltip placement="top" title={text}>
-          {text}
+        <Tooltip placement="topLeft" title={text}>
+          <div style={{ overflow: 'hidden', whiteSpace: 'nowrap', textOverflow: 'ellipsis' }}>
+            {text}
+          </div>
         </Tooltip>
       ),
     },
@@ -257,11 +246,11 @@ const Access = (props: Props) => {
       title: '示例',
       dataIndex: 'example',
       key: 'example',
-      ellipsis: true,
-      align: 'center',
       render: (text: any) => (
-        <Tooltip placement="top" title={text}>
-          {text}
+        <Tooltip placement="topLeft" title={text}>
+          <div style={{ overflow: 'hidden', whiteSpace: 'nowrap', textOverflow: 'ellipsis' }}>
+            {text}
+          </div>
         </Tooltip>
       ),
     },
@@ -269,11 +258,11 @@ const Access = (props: Props) => {
       title: '说明',
       dataIndex: 'description',
       key: 'description',
-      ellipsis: true,
-      align: 'center',
       render: (text: any) => (
-        <Tooltip placement="top" title={text}>
-          {text}
+        <Tooltip placement="topLeft" title={text}>
+          <div style={{ overflow: 'hidden', whiteSpace: 'nowrap', textOverflow: 'ellipsis' }}>
+            {text}
+          </div>
         </Tooltip>
       ),
     },

+ 32 - 25
src/pages/link/Protocol/save/index.tsx

@@ -1,4 +1,4 @@
-import { Button, message } from 'antd';
+import { Button, message, Modal } from 'antd';
 import { createForm, registerValidateRules } from '@formily/core';
 import { createSchemaField } from '@formily/react';
 import React, { useEffect, useState } from 'react';
@@ -6,9 +6,9 @@ import * as ICONS from '@ant-design/icons';
 import { Form, FormGrid, FormItem, Input, Select } from '@formily/antd';
 import type { ISchema } from '@formily/json-schema';
 import { service } from '@/pages/link/Protocol';
-import { Modal } from '@/components';
 import FileUpload from '../FileUpload';
 import type { ProtocolItem } from '@/pages/link/Protocol/typings';
+import { PermissionButton } from '@/components';
 
 interface Props {
   data: ProtocolItem | undefined;
@@ -18,6 +18,7 @@ interface Props {
 
 const Save = (props: Props) => {
   const [data, setData] = useState<ProtocolItem | undefined>(props.data);
+  const { permission } = PermissionButton.usePermission('link/Protocol');
 
   useEffect(() => {
     setData(props.data);
@@ -223,29 +224,35 @@ const Save = (props: Props) => {
       visible
       onCancel={props.close}
       width={700}
-      permissionCode={'link/Protocol'}
-      permission={['add', 'edit']}
-      footer={
-        <>
-          <Button onClick={props.close}>取消</Button>
-          <Button
-            type="primary"
-            onClick={() => {
-              save(false);
-            }}
-          >
-            保存
-          </Button>
-          <Button
-            type="primary"
-            onClick={() => {
-              save(true);
-            }}
-          >
-            保存并发布
-          </Button>
-        </>
-      }
+      footer={[
+        <Button key={1} onClick={props.close}>
+          取消
+        </Button>,
+        <Button
+          type="primary"
+          key={2}
+          onClick={() => {
+            save(false);
+          }}
+          disabled={props.data?.id ? !permission.update : !permission.add}
+        >
+          保存
+        </Button>,
+        <Button
+          key={3}
+          type="primary"
+          onClick={() => {
+            save(true);
+          }}
+          disabled={
+            props.data?.id
+              ? !permission.update && !permission.action
+              : !permission.add && !permission.action
+          }
+        >
+          保存并发布
+        </Button>,
+      ]}
     >
       <Form form={form} layout="vertical">
         <SchemaField schema={schema} />

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

@@ -28,6 +28,12 @@ const Channel = () => {
     if (resp.status === 200) {
       actionRef.current?.reload();
       message.success('操作成功!');
+      if (list.length === 1) {
+        const index = selectedRowKey.indexOf(list[0]);
+        const dt = [...selectedRowKey];
+        dt.splice(index, 1);
+        setSelectedRowKey([...dt]);
+      }
     }
   };
 
@@ -195,7 +201,7 @@ const Channel = () => {
         request={async (params) => {
           return service.queryBindChannel(id, {
             ...params,
-            sorts: [{ name: 'createTime', order: 'desc' }],
+            sorts: [{ name: 'name', order: 'desc' }],
           });
         }}
         rowKey="channelId"

+ 111 - 0
src/pages/notice/Config/BindUser/index.tsx

@@ -0,0 +1,111 @@
+import { createForm } from '@formily/core';
+import { createSchemaField } from '@formily/react';
+import { Form, FormItem, Select } from '@formily/antd';
+import { message, Modal } from 'antd';
+import { service, state } from '..';
+import { useEffect, useState } from 'react';
+
+interface Props {
+  close: () => void;
+  data: any;
+  id: string;
+  reload: () => void;
+}
+
+const BindUser = (props: Props) => {
+  const form = createForm({
+    validateFirst: true,
+    initialValues: { user: props.data?.userId || '' },
+  });
+  const [list, setList] = useState<any[]>([]);
+
+  const getUsers = async (id: string) => {
+    const resp = await service.syncUser.noBindUser({
+      pagign: false,
+      terms: [
+        {
+          column: `id$user-third${state.current?.type}_${state.current?.provider}$not`,
+          value: id,
+        },
+      ],
+    });
+    const data = resp.result?.map((item: Record<string, unknown>) => ({
+      label: item.name,
+      value: item.id,
+    }));
+    setList(data);
+  };
+
+  useEffect(() => {
+    if (props.data?.id) {
+      getUsers(props.data.id);
+    }
+  }, [props.data]);
+
+  const SchemaField = createSchemaField({
+    components: {
+      FormItem,
+      Select,
+    },
+  });
+
+  const schema = {
+    type: 'object',
+    properties: {
+      user: {
+        type: 'string',
+        title: '用户',
+        'x-decorator': 'FormItem',
+        'x-component': 'Select',
+        'x-component-props': {
+          placeholder: '请选择用户',
+          filterOption: (input: string, option: any) =>
+            option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0,
+        },
+        enum: [...list],
+        'x-validator': [
+          {
+            required: true,
+            message: '请选择用户',
+          },
+        ],
+      },
+    },
+  };
+
+  return (
+    <Modal
+      title="绑定用户 "
+      onCancel={() => {
+        props.close();
+      }}
+      visible
+      width={500}
+      onOk={async () => {
+        const values: any = (await form.submit()) as any;
+        const resp = await service.syncUser.bindUser(
+          props.id,
+          state.current?.provider || '',
+          state.current?.id || '',
+          [
+            {
+              userId: values.user,
+              providerName: props.data?.name,
+              thirdPartyUserId: props.data?.id,
+            },
+          ],
+        );
+        if (resp.status === 200) {
+          message.success('操作成功!');
+          props.reload();
+        }
+      }}
+    >
+      <Form form={form} layout="vertical">
+        <SchemaField schema={schema} />
+      </Form>
+    </Modal>
+  );
+};
+
+export default BindUser;

+ 4 - 6
src/pages/notice/Config/Debug/index.tsx

@@ -34,12 +34,10 @@ const Debug = observer(() => {
             const list = Store.get('notice-template-list');
 
             const _template = list.find((item: any) => item.id === value);
-            if (_template?.variableDefinitions?.length > 0) {
-              form1.setFieldState('variableDefinitions', (_state) => {
-                _state.visible = true;
-                _state.value = _template.variableDefinitions;
-              });
-            }
+            form1.setFieldState('variableDefinitions', (_state) => {
+              _state.visible = _template?.variableDefinitions?.length > 0;
+              _state.value = _template.variableDefinitions;
+            });
           });
           onFieldReact('variableDefinitions.*.type', (field) => {
             const value = (field as Field).value;

+ 74 - 3
src/pages/notice/Config/Detail/index.tsx

@@ -33,6 +33,7 @@ import AliyunVoice from '@/pages/notice/Config/Detail/doc/AliyunVoice';
 import Email from '@/pages/notice/Config/Detail/doc/Email';
 import { PermissionButton } from '@/components';
 import usePermissions from '@/hooks/permission';
+import FAutoComplete from '@/components/FAutoComplete';
 
 export const docMap = {
   weixin: {
@@ -105,6 +106,7 @@ const Detail = observer(() => {
       FUpload,
       Checkbox,
       NumberPicker,
+      FAutoComplete,
     },
   });
 
@@ -127,6 +129,12 @@ const Detail = observer(() => {
         'x-component-props': {
           placeholder: '请输入名称',
         },
+        'x-validator': [
+          {
+            max: 64,
+            message: '最多可输入64个字符',
+          },
+        ],
       },
       type: {
         title: '分类',
@@ -336,9 +344,23 @@ const Detail = observer(() => {
                     required: true,
                     'x-component-props': {
                       placeholder: '请输入服务器地址',
+                      style: {
+                        width: '200px',
+                      },
                     },
-                    'x-component': 'Input',
+                    'x-component': 'FAutoComplete',
                     'x-decorator': 'FormItem',
+                    enum: [
+                      { label: 'smtp.163.com', value: 'smtp.163.com' },
+                      { label: 'pop.163.com', value: 'pop.163.com' },
+                      { label: 'smtp.exmail.qq.com', value: 'smtp.exmail.qq.com' },
+                      { label: 'pop.exmail.qq.com', value: 'pop.exmail.qq.com' },
+                      { label: 'smtp.qq.com', value: 'smtp.qq.com' },
+                      { label: 'pop.qq.com', value: 'pop.qq.com' },
+                      { label: 'smtpdm.aliyun.com', value: 'smtpdm.aliyun.com' },
+                      { label: 'smtp.126.com', value: 'smtp.126.com' },
+                      { label: 'pop.126.com', value: 'pop.126.com' },
+                    ],
                   },
                   port: {
                     // title: '端口',
@@ -346,15 +368,40 @@ const Detail = observer(() => {
                     'x-component-props': {
                       placeholder: '请输入端口',
                     },
+                    default: 25,
+                    'x-validator': [
+                      {
+                        min: 1,
+                        max: 65535,
+                        message: '请输入1~65535之间的正整数',
+                      },
+                    ],
                     'x-component': 'NumberPicker',
                     'x-decorator': 'FormItem',
+                    'x-reactions': {
+                      dependencies: ['.enableSSL'],
+                      when: '{{$deps[0]}}',
+                      fulfill: {
+                        state: {
+                          value: 465,
+                        },
+                      },
+                      otherwise: {
+                        state: {
+                          value: 25,
+                        },
+                      },
+                    },
                   },
                   enableSSL: {
                     // title: '开启SSL',
                     type: 'boolean',
-                    'x-component': 'Checkbox.Group',
+                    'x-component': 'Checkbox',
                     'x-decorator': 'FormItem',
-                    enum: [{ label: '开启SSL', value: true }],
+                    'x-component-props': {
+                      children: '开启SSL',
+                    },
+                    // enum: [{label: '开启SSL', value: true}],
                   },
                 },
               },
@@ -366,6 +413,12 @@ const Detail = observer(() => {
                 'x-component-props': {
                   placeholder: '请输入发件人',
                 },
+                'x-validator': [
+                  {
+                    max: 64,
+                    message: '最多可输入64个字符',
+                  },
+                ],
               },
               username: {
                 title: '用户名',
@@ -375,6 +428,12 @@ const Detail = observer(() => {
                   placeholder: '请输入用户名',
                 },
                 'x-decorator': 'FormItem',
+                'x-validator': [
+                  {
+                    max: 64,
+                    message: '最多可输入64个字符',
+                  },
+                ],
               },
               password: {
                 title: '密码',
@@ -384,6 +443,12 @@ const Detail = observer(() => {
                 },
                 'x-component': 'Input',
                 'x-decorator': 'FormItem',
+                'x-validator': [
+                  {
+                    max: 64,
+                    message: '最多可输入64个字符',
+                  },
+                ],
               },
             },
           },
@@ -396,6 +461,12 @@ const Detail = observer(() => {
         'x-component-props': {
           rows: 4,
         },
+        'x-validator': [
+          {
+            max: 200,
+            message: '最多可输入200个字符',
+          },
+        ],
       },
     },
   };

+ 30 - 4
src/pages/notice/Config/Log/index.tsx

@@ -1,4 +1,4 @@
-import { Modal } from 'antd';
+import { Badge, Modal } from 'antd';
 import { observer } from '@formily/react';
 import { service, state } from '..';
 import ProTable, { ActionType, ProColumns } from '@jetlinks/pro-table';
@@ -17,13 +17,37 @@ const Log = observer(() => {
       title: 'ID',
     },
     {
-      dataIndex: 'sendTime',
+      dataIndex: 'notifyTime',
       title: '发送时间',
     },
     {
       dataIndex: 'state',
       title: '状态',
-      renderText: (text) => text.text,
+      renderText: (text: { value: string; text: string }, record) => {
+        return (
+          <>
+            <Badge status={text.value === 'success' ? 'success' : 'error'} text={text.text} />
+            {text.value !== 'success' && (
+              <a
+                style={{ marginLeft: 5 }}
+                key="info"
+                onClick={() => {
+                  Modal.info({
+                    title: '错误信息',
+                    width: '30vw',
+                    content: (
+                      <div style={{ height: '300px', overflowY: 'auto' }}>{record.errorStack}</div>
+                    ),
+                    onOk() {},
+                  });
+                }}
+              >
+                <InfoCircleOutlined />
+              </a>
+            )}
+          </>
+        );
+      },
     },
     {
       dataIndex: 'action',
@@ -35,7 +59,9 @@ const Log = observer(() => {
               title: '详情信息',
               width: '30vw',
               content: (
-                <div style={{ height: '300px', overflowY: 'auto' }}>{JSON.stringify(record)}</div>
+                <div style={{ height: '300px', overflowY: 'auto' }}>
+                  {JSON.stringify(record.context)}
+                </div>
               ),
               onOk() {},
             });

+ 87 - 67
src/pages/notice/Config/SyncUser/index.tsx

@@ -1,52 +1,90 @@
-import { Button, Col, Input, Modal, Row, Tree } from 'antd';
+import { Badge, Button, Col, Input, message, Modal, Popconfirm, Row, Tooltip, Tree } from 'antd';
 import { observer } from '@formily/react';
 import { service, state } from '..';
-import ProTable, { ActionType, ProColumns } from '@jetlinks/pro-table';
+import type { ActionType, ProColumns } from '@jetlinks/pro-table';
+import ProTable from '@jetlinks/pro-table';
 import { useEffect, useRef, useState } from 'react';
 import { history, useLocation } from 'umi';
-import { PermissionButton } from '@/components';
 import { DisconnectOutlined, EditOutlined } from '@ant-design/icons';
+import BindUser from '../BindUser';
 
 const SyncUser = observer(() => {
   const [dept, setDept] = useState<string>();
   const location = useLocation<{ id: string }>();
   const id = (location as any).query?.id;
+  const [visible, setVisible] = useState<boolean>(false);
+  const [current, setCurrent] = useState<any>({});
 
   const idMap = {
     dingTalk: '钉钉',
     weixin: '微信',
   };
+
+  const actionRef = useRef<ActionType>();
+
   const columns: ProColumns<any>[] = [
     {
       dataIndex: 'id',
-      title: `${idMap[id]}ID`,
+      title: `${idMap[id]}用户名`,
+      render: (text: any, record: any) => (
+        <span>
+          {text}({record?.name})
+        </span>
+      ),
     },
     {
-      dataIndex: 'name',
-      title: `${idMap[id]}用户名`,
+      dataIndex: 'userId',
+      title: `用户`,
+      render: (text: any, record: any) => (
+        <span>{record?.userId ? `${record?.username}(${record?.userName})` : '--'}</span>
+      ),
     },
     {
-      dataIndex: 'action',
+      dataIndex: 'status',
       title: '绑定状态',
-      render: () => [
-        <PermissionButton
-          tooltip={{
-            title: '绑定用户',
-          }}
-        >
-          <EditOutlined />
-        </PermissionButton>,
-        <PermissionButton
-          tooltip={{
-            title: '解绑用户',
-          }}
-        >
-          <DisconnectOutlined />
-        </PermissionButton>,
+      render: (text: any, record: any) => (
+        <Badge
+          status={record?.userId ? 'success' : 'error'}
+          text={record?.userId ? '已绑定' : '未绑定'}
+        />
+      ),
+    },
+    {
+      dataIndex: 'action',
+      title: '操作',
+      render: (text: any, record: any) => [
+        <Tooltip title={'绑定用户'} key="bind">
+          <Button
+            type="link"
+            onClick={() => {
+              setCurrent(record);
+              setVisible(true);
+            }}
+          >
+            <EditOutlined />
+          </Button>
+        </Tooltip>,
+        <Tooltip title={'解绑用户'} key="unbind">
+          <Button type="link">
+            <Popconfirm
+              title={'确认解绑'}
+              onConfirm={async () => {
+                if (record?.bindingId) {
+                  const resp = await service.syncUser.unBindUser(record.bindingId);
+                  if (resp.status === 200) {
+                    message.success('操作成功!');
+                    actionRef.current?.reload();
+                  }
+                }
+              }}
+            >
+              <DisconnectOutlined />
+            </Popconfirm>
+          </Button>
+        </Tooltip>,
       ],
     },
   ];
-  const actionRef = useRef<ActionType>();
 
   const [treeData, setTreeData] = useState([]);
 
@@ -60,7 +98,7 @@ const SyncUser = observer(() => {
           if (resp.status === 200) {
             setTreeData(resp.result);
             setDept(resp.result[0].id);
-            console.log(resp.result[0].id, 'id');
+            // console.log(resp.result[0].id, 'id');
           }
         });
       } else if (id === 'weixin') {
@@ -68,7 +106,7 @@ const SyncUser = observer(() => {
           if (resp.status === 200) {
             setTreeData(resp.result);
             setDept(resp.result[0].id);
-            console.log(resp.result[0].id, 'id~~');
+            // console.log(resp.result[0].id, 'id~~');
           }
         });
       }
@@ -82,40 +120,6 @@ const SyncUser = observer(() => {
     getDepartment();
   }, [id]);
 
-  // const updateTreeData = (list: any[], key: React.Key, children: any[]): any[] => {
-  //   return list.map((node) => {
-  //     if (node.id === key) {
-  //       return {
-  //         ...node,
-  //         children: node.children ? [...node.children, ...children] : children,
-  //       };
-  //     }
-  //
-  //     if (node.children) {
-  //       return {
-  //         ...node,
-  //         children: updateTreeData(node.children, key, children),
-  //       };
-  //     }
-  //     return node;
-  //   });
-  // };
-
-  // const getParentKey = (key: any, tree: string | any[]): any => {
-  //   let parentKey;
-  //   for (let i = 0; i < tree.length; i++) {
-  //     const node = tree[i];
-  //     if (node.children) {
-  //       if (node.children.some((item: { key: any; }) => item.key === key)) {
-  //         parentKey = node.key;
-  //       } else if (getParentKey(key, node.children)) {
-  //         parentKey = getParentKey(key, node.children);
-  //       }
-  //     }
-  //   }
-  //   return parentKey;
-  // };
-
   return (
     <Modal
       title="同步用户"
@@ -137,7 +141,6 @@ const SyncUser = observer(() => {
                 setDept(key[0] as string);
               }}
               treeData={treeData}
-              // loadData={onLoadData}
             />
           </div>
         </Col>
@@ -149,26 +152,28 @@ const SyncUser = observer(() => {
               search={false}
               columns={columns}
               params={{ dept: dept }}
-              request={async (params) =>
-                service.syncUser
-                  .getDeptUser(
+              request={(params) =>
+                service
+                  .queryZipSyncUser(
                     {
                       dingTalk: 'dingtalk',
                       weixin: 'wechat',
                     }[id],
+                    id,
+                    state.current?.provider || '',
                     state.current?.id || '',
                     params.dept || '',
                   )
-                  .then((resp) => {
+                  .then((resp: any) => {
                     return {
-                      code: resp.message,
+                      code: '',
                       result: {
-                        data: resp.result || [],
+                        data: resp || [],
                         pageIndex: 0,
                         pageSize: 0,
                         total: 0,
                       },
-                      status: resp.status,
+                      status: 200,
                     };
                   })
               }
@@ -177,6 +182,21 @@ const SyncUser = observer(() => {
           )}
         </Col>
       </Row>
+      {visible && (
+        <BindUser
+          id={id}
+          close={() => {
+            setCurrent({});
+            setVisible(false);
+          }}
+          data={current}
+          reload={() => {
+            setCurrent({});
+            setVisible(false);
+            actionRef.current?.reload();
+          }}
+        />
+      )}
     </Modal>
   );
 });

+ 46 - 0
src/pages/notice/Config/service.ts

@@ -1,6 +1,7 @@
 import BaseService from '@/utils/BaseService';
 import { request } from 'umi';
 import SystemConst from '@/utils/const';
+import { from, lastValueFrom, map, mergeMap, toArray, zip } from 'rxjs';
 
 class Service extends BaseService<ConfigItem> {
   public getTypes = () =>
@@ -64,6 +65,10 @@ class Service extends BaseService<ConfigItem> {
         method: 'PATCH',
         data,
       }),
+    bindUserThirdParty: (type: string, provider: string, configId: string) =>
+      request(`${SystemConst.API_BASE}/user/third-party/${type}_${provider}/${configId}`, {
+        method: 'GET',
+      }),
     getUserBindInfo: () =>
       request(`${SystemConst.API_BASE}/user/third-party/me`, { method: 'GET' }),
     unBindUser: (bindId: string) =>
@@ -71,6 +76,47 @@ class Service extends BaseService<ConfigItem> {
         method: 'DELETE',
       }),
   };
+
+  public queryZipSyncUser = (
+    type: 'wechat' | 'dingTalk',
+    _type: string,
+    provider: string,
+    configId: string,
+    departmentId: string,
+  ) =>
+    lastValueFrom(
+      zip(
+        from(this.syncUser.getDeptUser(type, configId, departmentId)),
+        from(this.syncUser.bindUserThirdParty(_type, provider, configId)),
+        from(this.syncUser.noBindUser({ paging: false })),
+      ).pipe(
+        map((resp) => resp.map((i) => i.result)),
+        mergeMap((res) => {
+          const [resp1, resp2, resp3] = res;
+          const list = resp1.map((item: { id: string; name: string }) => {
+            const data =
+              resp2.find(
+                (i: { userId: string; providerName: string; thirdPartyUserId: string }) =>
+                  i.thirdPartyUserId === item.id,
+              ) || {};
+            let _user: Partial<UserItem> = {};
+            if (data) {
+              _user = resp3.find((i: UserItem) => i.id === data.userId);
+            }
+            return {
+              ..._user,
+              ...data,
+              ...item,
+              bindingId: data?.id,
+              userId: _user?.id,
+              userName: _user?.name,
+            };
+          });
+          return list;
+        }),
+        toArray(),
+      ),
+    );
 }
 
 export default Service;

+ 192 - 51
src/pages/notice/Template/Detail/index.tsx

@@ -16,11 +16,18 @@ import {
   Switch,
 } from '@formily/antd';
 import type { Field } from '@formily/core';
-import { createForm, FormPath, onFieldInit, onFieldReact, onFieldValueChange } from '@formily/core';
+import {
+  createForm,
+  FormPath,
+  onFieldInit,
+  onFieldReact,
+  onFieldValueChange,
+  registerValidateRules,
+} from '@formily/core';
 import { createSchemaField, observer } from '@formily/react';
 import type { ISchema } from '@formily/json-schema';
 import styles from './index.less';
-import { useEffect, useMemo, useState } from 'react';
+import { useEffect, useMemo, useRef, useState } from 'react';
 import FUpload from '@/components/Upload';
 import { useParams } from 'umi';
 import { PageContainer } from '@ant-design/pro-layout';
@@ -37,6 +44,7 @@ import AliyunVoice from '@/pages/notice/Template/Detail/doc/AliyunVoice';
 import AliyunSms from '@/pages/notice/Template/Detail/doc/AliyunSms';
 import Email from '@/pages/notice/Template/Detail/doc/Email';
 import { Store } from 'jetlinks-store';
+import FAutoComplete from '@/components/FAutoComplete';
 
 export const docMap = {
   weixin: {
@@ -95,6 +103,8 @@ const Detail = observer(() => {
   const getAliyunSigns = (configId: string) => service.aliyun.getSigns(configId);
   const getAliyunTemplates = (configId: string) => service.aliyun.getTemplates(configId);
 
+  const variableDefinitionsRef =
+    useRef<{ id: string; name: string; type: string; format: string }[]>();
   const form = useMemo(
     () =>
       createForm({
@@ -105,6 +115,7 @@ const Detail = observer(() => {
               field.setComponent(FBraftEditor, {
                 placeholder:
                   '变量格式:${name};\n 示例:尊敬的${name},${time}有设备触发告警,请注意处理',
+                height: '100px',
               });
             }
           });
@@ -119,6 +130,14 @@ const Detail = observer(() => {
             form1.setFieldState('configId', async (state1) => {
               state1.dataSource = await getConfig(value);
             });
+
+            if (value === 'officialMessage') {
+              form1.setFieldState('template.message', (state5) => {
+                state5.decoratorProps = {
+                  tooltip: '服务号模版消息内容',
+                };
+              });
+            }
           });
           onFieldValueChange('configId', (field, form1) => {
             const value = field.value;
@@ -210,21 +229,82 @@ const Detail = observer(() => {
               });
             }
           });
+          onFieldValueChange('template.*(subject,markdown.title)', (field, form1) => {
+            const value = (field as Field).value;
+            const _message = field.query('template.message').value();
+
+            const titleList =
+              (typeof value === 'string' &&
+                (value + _message)?.match(pattern)?.filter((i: string) => i)) ||
+              // .map((item: string) => ({id: item, type: 'string', format: '--'}))) ||
+              [];
+            // 拼接message的内容
+
+            form1.setFieldState('variableDefinitions', (state1) => {
+              state1.visible = !!titleList && titleList.length > 0;
+            });
+            if (form1.modified) {
+              const oldKey = variableDefinitionsRef.current?.map((i) => i.id);
+              const newKey = [...new Set(titleList)];
+              const _result = newKey.map((item) =>
+                oldKey?.includes(item)
+                  ? variableDefinitionsRef.current?.find((i) => i.id === item)
+                  : {
+                      id: item,
+                      type: 'string',
+                      format: '--',
+                    },
+              );
+              form1.setValuesIn('variableDefinitions', _result);
+            }
+          });
           onFieldValueChange('template.message', (field, form1) => {
             const value = (field as Field).value;
             const idList =
-              typeof value === 'string' &&
-              value
-                ?.match(pattern)
-                ?.filter((i: string) => i)
-                .map((item: string) => ({ id: item, type: 'string', format: '--' }));
+              (typeof value === 'string' && value?.match(pattern)?.filter((i: string) => i)) || [];
+
+            if (id === 'email') {
+              const subject = field.query('template.subject');
+              const title = subject.value();
+              const titleList = title?.match(pattern)?.filter((i: string) => i);
+              // .map((item: string) => ({id: item, type: 'string', format: '--'}));
+              if (idList && titleList?.length > 0) {
+                idList.unshift(...titleList);
+              }
+            }
+            const _provider = field.query('provider').value();
+            if (_provider === 'dingTalkRobotWebHook') {
+              const title = field.query('template.markdown.title').value();
+              const titleList = title?.match(pattern)?.filter((i: string) => i);
+              // .map((item: string) => ({id: item, type: 'string', format: '--'}));
+              if (idList && titleList?.length > 0) {
+                idList.unshift(...titleList);
+              }
+            }
             form1.setFieldState('variableDefinitions', (state1) => {
               state1.visible = !!idList && idList.length > 0;
             });
+
             if (form1.modified) {
-              form1.setValuesIn('variableDefinitions', idList);
+              // 获取缓存的KEY;
+              const oldKey = variableDefinitionsRef.current?.map((i) => i.id);
+              const newKey = [...new Set(idList)];
+              const _result = newKey.map((item) =>
+                oldKey?.includes(item)
+                  ? variableDefinitionsRef.current?.find((i) => i.id === item)
+                  : {
+                      id: item,
+                      type: 'string',
+                      format: '--',
+                    },
+              );
+              form1.setValuesIn('variableDefinitions', _result);
             }
           });
+          onFieldValueChange('variableDefinitions.*.*', (field) => {
+            // 缓存编辑后的数据
+            variableDefinitionsRef.current = field.query('variableDefinitions').value();
+          });
           onFieldReact('variableDefinitions.*.type', (field) => {
             const value = (field as Field).value;
             const formatPath = FormPath.transform(
@@ -233,41 +313,51 @@ const Detail = observer(() => {
               (index) => `variableDefinitions.${parseInt(index)}.format`,
             );
             const format = field.query(formatPath).take() as any;
+
+            const fieldModified = field && (field as Field).modified;
             if (!format) return;
+            if (fieldModified) {
+              format.setValue(undefined);
+            }
             switch (value) {
               case 'date':
-                format.setComponent(Select);
+                format.setComponent(FAutoComplete);
                 format.setDataSource([
-                  { label: 'String类型的UTC时间戳 (毫秒)', value: 'string' },
+                  { label: 'timestamp', value: 'timestamp' },
                   { label: 'yyyy-MM-dd', value: 'yyyy-MM-dd' },
                   { label: 'yyyy-MM-dd HH:mm:ss', value: 'yyyy-MM-dd HH:mm:ss' },
                   { label: 'yyyy-MM-dd HH:mm:ss EE', value: 'yyyy-MM-dd HH:mm:ss EE' },
                   { label: 'yyyy-MM-dd HH:mm:ss zzz', value: 'yyyy-MM-dd HH:mm:ss zzz' },
                 ]);
-                format.setValue('string');
+                if (fieldModified) {
+                  format.setValue('timestamp');
+                }
                 break;
               case 'string':
-                console.log('string');
                 format.setComponent(PreviewText.Input);
-                format.setValue('%s');
+                if (fieldModified) {
+                  format.setValue('s%');
+                }
                 break;
               case 'number':
                 format.setComponent(Input);
-                format.setValue('%.xf');
-                break;
-              case 'file':
-                format.setComponent(Select);
-                format.setDataSource([
-                  { label: '视频', value: 'video' },
-                  { label: '图片', value: 'img' },
-                  { label: '全部', value: 'any' },
-                ]);
-                format.setValue('any');
-                break;
-              case 'other':
-                format.setComponent(PreviewText.Input);
-                format.setValue('--');
+                if (fieldModified) {
+                  format.setValue('%.xf');
+                }
                 break;
+              // case 'file':
+              //   format.setComponent(Select);
+              //   format.setDataSource([
+              //     {label: '视频', value: 'video'},
+              //     {label: '图片', value: 'img'},
+              //     {label: '全部', value: 'any'},
+              //   ]);
+              //   format.setValue('any');
+              //   break;
+              // case 'other':
+              //   format.setComponent(PreviewText.Input);
+              //   format.setValue('--');
+              //   break;
             }
           });
         },
@@ -297,6 +387,7 @@ const Detail = observer(() => {
       ArrayItems,
       FormGrid,
       ArrayTable,
+      FAutoComplete,
     },
   });
 
@@ -347,7 +438,20 @@ const Detail = observer(() => {
     }
   };
 
-  console.log(typeList[id][0]);
+  registerValidateRules({
+    batchCheckEmail(value) {
+      const regEmail = /^([a-zA-Z]|[0-9])(\w|\-)+@[a-zA-Z0-9]+\.([a-zA-Z]{2,4})$/;
+      let error;
+      value.some((item: string) => {
+        if (!regEmail.test(item)) {
+          error = item;
+          return true;
+        }
+        return false;
+      });
+      return error ? `${error}邮件格式错误` : '';
+    },
+  });
   const schema: ISchema = {
     type: 'object',
     properties: {
@@ -399,6 +503,10 @@ const Detail = observer(() => {
         'x-component-props': {
           placeholder: '请选择绑定配置',
         },
+        required: true,
+        'x-decorator-props': {
+          tooltip: '使用固定的通知配置来发送此通知模版',
+        },
         'x-visible': id !== 'email',
       },
       template: {
@@ -416,8 +524,9 @@ const Detail = observer(() => {
                     'x-component': 'Input',
                     'x-decorator': 'FormItem',
                     'x-decorator-props': {
-                      tooltip: '请输入AgentID',
+                      tooltip: '应用唯一标识',
                     },
+                    required: true,
                     'x-component-props': {
                       placeholder: '请输入AgentID',
                     },
@@ -435,11 +544,11 @@ const Detail = observer(() => {
                         'x-component': 'Select',
                         'x-decorator': 'FormItem',
                         'x-decorator-props': {
-                          tooltip: '请输入收信人ID',
+                          tooltip: '如果不填写该字段,将在使用此模版发送通知时进行指定。',
                           gridSpan: 1,
                         },
                         'x-component-props': {
-                          placeholder: '请输入收信人ID',
+                          placeholder: '请选择收信人',
                         },
                       },
                       toParty: {
@@ -447,11 +556,11 @@ const Detail = observer(() => {
                         'x-component': 'Select',
                         'x-decorator': 'FormItem',
                         'x-decorator-props': {
-                          tooltip: '请输入收信部门ID',
+                          tooltip: '如果不填写该字段,将在使用此模版发送通知时进行指定。',
                           gridSpan: 1,
                         },
                         'x-component-props': {
-                          placeholder: '请输入收信部门ID',
+                          placeholder: '请选择收信部门',
                         },
                       },
                     },
@@ -461,7 +570,8 @@ const Detail = observer(() => {
                     'x-component': 'Select',
                     'x-decorator': 'FormItem',
                     'x-decorator-props': {
-                      tooltip: '标签推送',
+                      tooltip:
+                        '本企业微信的标签ID列表,最多支持100个,如果不填写该字段,将在使用此模版发送通知时进行指定',
                     },
                     'x-component-props': {
                       placeholder: '请输入标签推送,多个标签用,号分隔',
@@ -488,6 +598,9 @@ const Detail = observer(() => {
                     'x-component-props': {
                       placeholder: '请选择用户标签',
                     },
+                    'x-decorator-props': {
+                      tooltip: '如果不填写该字段,将在使用此模板发送通知时进行指定',
+                    },
                   },
                   layout: {
                     type: 'void',
@@ -507,6 +620,7 @@ const Detail = observer(() => {
                         },
                         'x-decorator-props': {
                           gridSpan: 1,
+                          tooltip: '微信公众号中配置的消息模版',
                         },
                       },
                       url: {
@@ -519,6 +633,7 @@ const Detail = observer(() => {
                         },
                         'x-decorator-props': {
                           gridSpan: 1,
+                          tooltip: '用于点击消息后进行页面跳转',
                         },
                       },
                     },
@@ -531,6 +646,9 @@ const Detail = observer(() => {
                     'x-component-props': {
                       // optionType: 'button'
                     },
+                    'x-decorator-props': {
+                      tooltip: '配置后点击通知消息将跳转到对应小程序',
+                    },
                     default: false,
                     enum: [
                       { label: '是', value: true },
@@ -558,6 +676,7 @@ const Detail = observer(() => {
                             },
                             'x-decorator-props': {
                               gridSpan: 1,
+                              tooltip: '小程序唯一性id',
                             },
                           },
                           miniProgramPath: {
@@ -570,6 +689,7 @@ const Detail = observer(() => {
                             },
                             'x-decorator-props': {
                               gridSpan: 1,
+                              tooltip: '用于点击消息之后跳转到小程序的具体页面',
                             },
                           },
                         },
@@ -592,6 +712,9 @@ const Detail = observer(() => {
                     'x-component-props': {
                       placeholder: '这里是回显内容',
                     },
+                    'x-decorator-props': {
+                      tooltip: '服务号消息模版标题',
+                    },
                   },
                 },
                 'x-reactions': {
@@ -618,7 +741,7 @@ const Detail = observer(() => {
                     'x-component': 'Input',
                     'x-decorator': 'FormItem',
                     'x-decorator-props': {
-                      tooltip: '请输入AgentID',
+                      tooltip: '应用唯一标识',
                     },
                     'x-component-props': {
                       placeholder: '请输入AgentID',
@@ -637,11 +760,11 @@ const Detail = observer(() => {
                         'x-component': 'Select',
                         'x-decorator': 'FormItem',
                         'x-decorator-props': {
-                          tooltip: '请输入收信人ID',
+                          tooltip: '如果不填写该字段,将在使用此模板发送通知时进行指定',
                           gridSpan: 1,
                         },
                         'x-component-props': {
-                          placeholder: '请输入收信人ID',
+                          placeholder: '请选择收信人',
                         },
                         'x-reactions': {
                           dependencies: ['configId'],
@@ -655,7 +778,7 @@ const Detail = observer(() => {
                         'x-component': 'Select',
                         'x-decorator': 'FormItem',
                         'x-decorator-props': {
-                          tooltip: '收信部门ID',
+                          tooltip: '如果不填写该字段,将在使用此模板发送通知时进行指定',
                           gridSpan: 1,
                         },
                         'x-component-props': {
@@ -794,9 +917,10 @@ const Detail = observer(() => {
                         'x-component': 'Select',
                         'x-decorator': 'FormItem',
                         'x-decorator-props': {
-                          tooltip: '请输入模版ID',
+                          tooltip: '阿里云内部分配的唯一ID标识',
                           gridSpan: 1,
                         },
+                        required: true,
                         'x-component-props': {
                           placeholder: '请输入模版ID',
                         },
@@ -806,7 +930,7 @@ const Detail = observer(() => {
                         'x-component': 'Input',
                         'x-decorator': 'FormItem',
                         'x-decorator-props': {
-                          tooltip: '请输入被叫号码',
+                          tooltip: '仅支持中国大陆号码',
                           gridSpan: 1,
                         },
                         'x-component-props': {
@@ -820,7 +944,7 @@ const Detail = observer(() => {
                     'x-component': 'Input',
                     'x-decorator': 'FormItem',
                     'x-decorator-props': {
-                      tooltip: '请输入被叫显号',
+                      tooltip: '必须是已购买的号码,用于呼叫号码显示',
                     },
                     'x-component-props': {
                       placeholder: '请输入被叫显号',
@@ -828,11 +952,19 @@ const Detail = observer(() => {
                   },
                   PlayTimes: {
                     title: '播放次数',
-                    'x-component': 'Input',
+                    'x-component': 'NumberPicker',
                     'x-decorator': 'FormItem',
                     'x-decorator-props': {
-                      tooltip: '请输入播放次数',
+                      tooltip: '语音文件的播放次数',
                     },
+                    default: 1,
+                    'x-validator': [
+                      {
+                        min: 1,
+                        max: 3,
+                        message: '仅支持1~3次',
+                      },
+                    ],
                     'x-component-props': {
                       placeholder: '请输入播放次数',
                     },
@@ -874,9 +1006,10 @@ const Detail = observer(() => {
                         'x-component': 'Input',
                         'x-decorator': 'FormItem',
                         'x-decorator-props': {
-                          tooltip: '请输入收信人',
+                          tooltip: '仅支持中国大陆号码',
                           gridSpan: 1,
                         },
+                        'x-validator': ['phone'],
                         'x-component-props': {
                           placeholder: '请输入收信人',
                         },
@@ -888,7 +1021,7 @@ const Detail = observer(() => {
                     'x-component': 'Select',
                     'x-decorator': 'FormItem',
                     'x-decorator-props': {
-                      tooltip: '请输入签名',
+                      tooltip: '用于短信内容签名信息显示',
                     },
                     'x-component-props': {
                       placeholder: '请输入签名',
@@ -913,22 +1046,27 @@ const Detail = observer(() => {
                 'x-decorator': 'FormItem',
                 title: '标题',
                 'x-decorator-props': {
-                  tip: '邮件标题',
+                  tooltip: '邮件标题',
                 },
+                required: true,
                 'x-component-props': {
                   placeholder: '请输入标题',
                 },
               },
               sendTo: {
-                'x-component': 'Input.TextArea',
+                'x-component': 'Select',
                 'x-decorator': 'FormItem',
                 title: '收件人',
                 'x-decorator-props': {
-                  tip: '多个收件人用换行分隔 \n最大支持1000个号码',
+                  tooltip: '多个收件人用换行分隔 \n最大支持1000个号码',
                 },
                 'x-component-props': {
+                  mode: 'tags',
                   placeholder: '请输入收件人邮箱,多个收件人用换行分隔',
                 },
+                'x-validator': {
+                  batchCheckEmail: true,
+                },
               },
               attachments: {
                 type: 'array',
@@ -939,6 +1077,7 @@ const Detail = observer(() => {
                   style: {
                     width: '100%',
                   },
+                  tooltip: '附件只输入文件名称将在发送邮件时进行文件上传',
                 },
                 items: {
                   type: 'object',
@@ -1002,7 +1141,6 @@ const Detail = observer(() => {
             },
           },
         },
-
         'x-component-props': {
           rows: 5,
           placeholder: '变量格式:${name};\n 示例:尊敬的${name},${time}有设备触发告警,请注意处理',
@@ -1017,6 +1155,11 @@ const Detail = observer(() => {
           pagination: { pageSize: 9999 },
           scroll: { x: '100%' },
         },
+        'x-decorator-props': {
+          style: {
+            zIndex: 999,
+          },
+        },
         'x-visible': false,
         items: {
           type: 'object',
@@ -1061,8 +1204,6 @@ const Detail = observer(() => {
                     { label: '字符串', value: 'string' },
                     { label: '时间', value: 'date' },
                     { label: '数字', value: 'number' },
-                    { label: '文件', value: 'file' },
-                    { label: '其他', value: 'other' },
                   ],
                 },
               },

+ 8 - 2
src/pages/notice/Template/Log/index.tsx

@@ -18,8 +18,9 @@ const Log = observer(() => {
       width: 200,
     },
     {
-      dataIndex: 'sendTime',
+      dataIndex: 'notifyTime',
       title: '发送时间',
+      valueType: 'dateTime',
     },
     {
       dataIndex: 'state',
@@ -30,6 +31,7 @@ const Log = observer(() => {
             <Badge status={text.value === 'success' ? 'success' : 'error'} text={text.text} />
             {text.value !== 'success' && (
               <a
+                style={{ marginLeft: 5 }}
                 key="info"
                 onClick={() => {
                   Modal.info({
@@ -59,7 +61,11 @@ const Log = observer(() => {
             Modal.info({
               title: '详情信息',
               width: '30vw',
-              content: <div style={{ height: '300px', overflowY: 'auto' }}>{record.message}</div>,
+              content: (
+                <div style={{ height: '300px', overflowY: 'auto' }}>
+                  {JSON.stringify(record.context)}
+                </div>
+              ),
               onOk() {},
             });
           }}

+ 1 - 1
src/pages/notice/Template/typings.d.ts

@@ -16,5 +16,5 @@ type LogItem = {
   sendTime: number;
   state: string;
   errorStack?: string;
-  message?: string;
+  context?: string;
 };

+ 5 - 2
src/pages/rule-engine/Alarm/Log/SolveLog/index.tsx

@@ -4,6 +4,7 @@ import ProTable from '@jetlinks/pro-table';
 import { Modal } from 'antd';
 import { useRef, useState } from 'react';
 import { service } from '@/pages/rule-engine/Alarm/Log';
+import moment from 'moment';
 
 interface Props {
   data: Partial<AlarmLogItem>;
@@ -35,8 +36,9 @@ const SolveLog = (props: Props) => {
 
   const columns: ProColumns<AlarmLogSolveHistoryItem>[] = [
     {
-      dataIndex: 'createTime',
+      dataIndex: 'handleTIme',
       title: '处理时间',
+      render: (text: any) => <span>{moment(text).format('YYYY-MM-DD HH:mm:ss')}</span>,
     },
     {
       dataIndex: 'handleType',
@@ -44,8 +46,9 @@ const SolveLog = (props: Props) => {
       render: (text: any) => <span>{typeMap.get(text) || ''}</span>,
     },
     {
-      dataIndex: 'address',
+      dataIndex: 'createTime',
       title: '告警时间',
+      render: (text: any) => <span>{moment(text).format('YYYY-MM-DD HH:mm:ss')}</span>,
     },
     {
       dataIndex: 'description',

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

@@ -152,7 +152,7 @@ const TabComponent = observer((props: Props) => {
                           item.level}
                       </div>
                     </div>
-                    <div className="alarm-log-title-text">{item.name}</div>
+                    <div className="alarm-log-title-text">{item.alarmName}</div>
                   </div>
                   <div className="alarm-log-content">
                     <div className="alarm-log-image">
@@ -203,6 +203,7 @@ const TabComponent = observer((props: Props) => {
                               <div className="btn">
                                 <ToolFilled className="icon" />
                                 <div>告警处理</div>
+                                {/* action */}
                               </div>
                             </Button>
                           </div>

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

@@ -1,7 +1,7 @@
 import { PageContainer } from '@ant-design/pro-layout';
 import { Button, Card, Form, Input } from 'antd';
 import { useLocation } from 'umi';
-import { useEffect, useState } from 'react';
+import { useEffect, useRef, useState } from 'react';
 import { PermissionButton } from '@/components';
 import ActionItems from './action/action';
 import { PlusOutlined } from '@ant-design/icons';
@@ -11,6 +11,7 @@ import TriggerTerm from '@/pages/rule-engine/Scene/TriggerTerm';
 export default () => {
   const location = useLocation();
   const [form] = Form.useForm();
+  const triggerRef = useRef<any>();
 
   const { getOtherPermission } = PermissionButton.usePermission('rule-engine/Scene');
   const [triggerType, setTriggerType] = useState('');
@@ -29,9 +30,60 @@ export default () => {
 
   const saveData = async () => {
     const formData = await form.validateFields();
+    // 获取触发条件数据
+    const triggerData = await triggerRef.current.getTriggerForm();
+    console.log(JSON.stringify(triggerData), 'trigger');
     console.log(formData);
   };
 
+  const [triggerValue, setTriggerValue] = useState<any>();
+  const requestParams = {
+    trigger: {
+      type: 'device',
+      device: {
+        productId: '0412-zj',
+        selector: 'device',
+        selectorValue: [
+          {
+            id: '0412-zj',
+            name: '0412-zj',
+          },
+        ],
+        operation: {
+          operator: 'reportProperty',
+          timer: {
+            trigger: 'week',
+            cron: '',
+            when: [1, 3, 5],
+            mod: 'period',
+            period: {
+              from: '09:30',
+              to: '14:30',
+              every: 1,
+              unit: 'hours',
+            },
+            once: {
+              time: '',
+            },
+          },
+          eventId: '',
+          readProperties: ['temparature', 'temperature-k', 'test-zhibioa'],
+          writeProperties: {},
+          functionId: '',
+          functionParameters: [
+            {
+              name: '',
+              value: {},
+            },
+          ],
+        },
+        defaultVariable: [],
+      },
+      timer: {},
+      defaultVariable: [],
+    },
+  };
+
   return (
     <PageContainer>
       <Card>
@@ -50,6 +102,14 @@ export default () => {
               <TriggerWay onSelect={setTriggerType} />
             </Form.Item>
           </Form.Item>
+          <Form.Item noStyle>
+            <TriggerTerm
+              ref={triggerRef}
+              params={requestParams}
+              value={triggerValue}
+              onChange={console.log}
+            />
+          </Form.Item>
           <Form.Item label={'执行动作'}>
             <Form.List name="actions">
               {(fields, { add, remove }) => (
@@ -77,9 +137,36 @@ export default () => {
         <PermissionButton isPermission={getOtherPermission(['add', 'update'])} onClick={saveData}>
           保存
         </PermissionButton>
-      </Card>
-      <Card>
-        <TriggerTerm />
+        <Button
+          onClick={() => {
+            setTriggerValue({
+              trigger: [
+                {
+                  terms: [
+                    {
+                      column: '_now',
+                      termType: 'eq',
+                      source: 'manual',
+                      value: '2022-04-21 14:26:04',
+                    },
+                  ],
+                },
+                {
+                  terms: [
+                    {
+                      column: 'properties.test-zhibioa.recent',
+                      termType: 'lte',
+                      source: 'metrics',
+                      value: '123',
+                    },
+                  ],
+                },
+              ],
+            });
+          }}
+        >
+          设置
+        </Button>
       </Card>
     </PageContainer>
   );

+ 136 - 24
src/pages/rule-engine/Scene/TriggerTerm/index.tsx

@@ -2,22 +2,128 @@ import { createSchemaField } from '@formily/react';
 import {
   ArrayCards,
   ArrayItems,
+  DatePicker,
   Form,
   FormGrid,
   FormItem,
   Input,
+  NumberPicker,
   Select,
   Space,
+  Switch,
+  TreeSelect,
 } from '@formily/antd';
 import { ISchema } from '@formily/json-schema';
-import { createForm } from '@formily/core';
-import { useMemo } from 'react';
+import { createForm, onFieldValueChange } from '@formily/core';
+import { forwardRef, useImperativeHandle, useMemo } from 'react';
 import FTermArrayCards from '@/components/FTermArrayCards';
 import FTermTypeSelect from '@/components/FTermTypeSelect';
 import styles from './index.less';
+import Service from '@/pages/rule-engine/Scene/service';
+import { useAsyncDataSource } from '@/utils/util';
+import { Store } from 'jetlinks-store';
+import { treeFilter } from '@/utils/tree';
 
-const TriggerTerm = () => {
-  const form = useMemo(() => createForm({}), []);
+const service = new Service('scene');
+
+interface Props {
+  // 查询下拉框的参数
+  params: Record<string, any>;
+  value?: Record<string, any>;
+  onChange?: (value: any) => void;
+}
+
+const TriggerTerm = (props: Props, ref: any) => {
+  const form = useMemo(
+    () =>
+      createForm({
+        validateFirst: true,
+        initialValues: props.value,
+        effects() {
+          onFieldValueChange('trigger.*.terms.*.column', (field, form1) => {
+            const operator = field.query('.termType');
+            // 找到选中的
+            const _data = Store.get('trigger-parse-term');
+            // 树形搜索
+            const treeValue = treeFilter(_data, field.value, 'column');
+            // 找到
+            const target =
+              treeValue && treeValue[0].children
+                ? treeValue[0]?.children.find((item) => item.column === field.value)
+                : treeValue[0];
+
+            form1.setFieldState(operator, (state) => {
+              state.dataSource = target?.termTypes?.map((item: any) => ({
+                label: item.name,
+                value: item.id,
+              }));
+            });
+            form1.setFieldState(field.query('.source'), (state) => {
+              state.dataSource =
+                target && target.metrics && target.metrics.length > 0
+                  ? [
+                      { label: '手动输入', value: 'manual' },
+                      { label: '指标', value: 'metrics' },
+                    ]
+                  : [{ label: '手动输入', value: 'manual' }];
+            });
+          });
+          onFieldValueChange('trigger.*.terms.*.source', (field, form1) => {
+            const params = field.query('.column').value();
+            const value = field.query('.value');
+            // 找到选中的
+            const _data = Store.get('trigger-parse-term');
+            // 树形搜索
+            const treeValue = treeFilter(_data, params, 'column');
+            // 找到
+            const target =
+              treeValue && treeValue[0].children
+                ? treeValue[0]?.children.find((item) => item.column === params)
+                : treeValue[0];
+
+            if (target) {
+              if (field.value === 'manual') {
+                // 手动输入
+                const valueType = target.dataType;
+
+                const valueTypeMap = {
+                  int: NumberPicker,
+                  float: NumberPicker,
+                  double: NumberPicker,
+                  long: NumberPicker,
+                  string: Input,
+                  date: DatePicker,
+                  boolean: Switch,
+                };
+
+                form1.setFieldState(value, (state) => {
+                  state.componentType = valueTypeMap[valueType];
+                  if (valueType === 'date') {
+                    state.componentProps = {
+                      showTime: true,
+                    };
+                  }
+                });
+              } else if (field.value === 'metrics') {
+                // 指标
+                form1.setFieldState(value, (state) => {
+                  state.componentType = Select;
+                  state.dataSource = target?.metrics.map((item: any) => ({
+                    label: item.name,
+                    value: item.id,
+                  }));
+                });
+              }
+            }
+          });
+        },
+      }),
+    [props.value],
+  );
+
+  useImperativeHandle(ref, () => ({
+    getTriggerForm: () => form.submit(),
+  }));
   const SchemaField = createSchemaField({
     components: {
       FormItem,
@@ -29,9 +135,20 @@ const TriggerTerm = () => {
       Space,
       FormGrid,
       FTermTypeSelect,
+      TreeSelect,
     },
   });
 
+  const getParseTerm = () =>
+    service.getParseTerm(props.params).then((data) => {
+      Store.set('trigger-parse-term', data);
+      return data.map((item: any) => ({
+        column: item.column,
+        name: item.name,
+        children: item.children,
+      }));
+    });
+
   const schema: ISchema = {
     type: 'object',
     properties: {
@@ -56,7 +173,8 @@ const TriggerTerm = () => {
               items: {
                 type: 'object',
                 properties: {
-                  termType: {
+                  // 关联类型
+                  type: {
                     type: 'string',
                     // "x-decorator": 'FormItem',
                     'x-component': 'FTermTypeSelect',
@@ -69,52 +187,45 @@ const TriggerTerm = () => {
                       minColumns: 24,
                     },
                     properties: {
-                      params: {
+                      // columns
+                      column: {
                         type: 'string',
                         // title: '日期',
                         'x-decorator': 'FormItem',
-                        'x-component': 'Input',
+                        'x-component': 'TreeSelect',
                         'x-decorator-props': {
                           gridSpan: 4,
                         },
                         'x-component-props': {
                           placeholder: '请选择参数',
+                          fieldNames: { value: 'column', label: 'name', options: 'children' },
                         },
+                        'x-reactions': '{{useAsyncDataSource(getParseTerm)}}',
                       },
-                      operator: {
+                      termType: {
                         type: 'string',
                         // title: '输入框',
                         'x-decorator': 'FormItem',
                         'x-component': 'Select',
                         'x-decorator-props': {
-                          gridSpan: 3,
+                          gridSpan: 1,
                         },
                         'x-component-props': {
                           placeholder: '操作符',
                         },
                       },
-                      type: {
+                      source: {
                         type: 'string',
                         'x-decorator': 'FormItem',
                         'x-component': 'Select',
-                        enum: [
-                          { label: '手动输入', value: 'sd' },
-                          { label: '指标', value: 'metrics' },
-                        ],
                         'x-decorator-props': {
-                          gridSpan: 3,
+                          gridSpan: 1,
                         },
                       },
                       value: {
                         type: 'string',
-                        enum: [
-                          { label: '高高值', value: 1 },
-                          { label: '低低值', value: 2 },
-                          { label: '高值', value: 3 },
-                          { label: '低值', value: 4 },
-                        ],
                         'x-decorator': 'FormItem',
-                        'x-component': 'Select',
+                        'x-component': 'Input',
                         'x-component-props': {},
                         'x-decorator-props': {
                           gridSpan: 3,
@@ -158,8 +269,9 @@ const TriggerTerm = () => {
   };
   return (
     <Form form={form} layout="vertical" className={styles.form}>
-      <SchemaField schema={schema} />
+      <SchemaField schema={schema} scope={{ useAsyncDataSource, getParseTerm }} />
     </Form>
   );
 };
-export default TriggerTerm;
+
+export default forwardRef(TriggerTerm);

+ 6 - 0
src/pages/rule-engine/Scene/service.ts

@@ -4,6 +4,12 @@ import type { SceneItem } from '@/pages/rule-engine/Scene/typings';
 
 class Service extends BaseService<SceneItem> {
   start = (id: string) => request(`${this.uri}/${id}`, { methods: 'GET' });
+
+  getParseTerm = (data: Record<string, any>) =>
+    request(`${this.uri}/parse-term-column`, {
+      method: 'POST',
+      data,
+    }).then((resp) => resp.result);
 }
 
 export default Service;

+ 27 - 23
src/pages/system/Relationship/Save/index.tsx

@@ -41,7 +41,21 @@ const Save = (props: Props) => {
 
   const form = createForm({
     validateFirst: true,
-    initialValues: props.data,
+    initialValues: {
+      ...props.data,
+      object: props.data?.objectType
+        ? JSON.stringify({
+            objectType: props.data.objectType,
+            objectTypeName: props.data.objectTypeName,
+          })
+        : undefined,
+      target: props.data?.targetType
+        ? JSON.stringify({
+            targetType: props.data.targetType,
+            targetTypeName: props.data.targetTypeName,
+          })
+        : undefined,
+    },
   });
 
   const SchemaField = createSchemaField({
@@ -114,28 +128,6 @@ const Save = (props: Props) => {
                 required: true,
                 message: '请输入标识',
               },
-              // {
-              //   triggerType: 'onBlur',
-              //   // validator: (value: string) => {
-              //   //   return new Promise((resolve) => {
-              //   //     service
-              //   //       .validateField('username', value)
-              //   //       .then((resp) => {
-              //   //         if (resp.status === 200) {
-              //   //           if (resp.result.passed) {
-              //   //             resolve('');
-              //   //           } else {
-              //   //             resolve(model === 'edit' ? '' : resp.result.reason);
-              //   //           }
-              //   //         }
-              //   //         resolve('');
-              //   //       })
-              //   //       .catch(() => {
-              //   //         return '验证失败!';
-              //   //       });
-              //   //   });
-              //   // },
-              // },
             ],
             name: 'relation',
             required: true,
@@ -156,6 +148,12 @@ const Save = (props: Props) => {
                 option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0,
             },
             required: true,
+            'x-validator': [
+              {
+                required: true,
+                message: '请选择关联方',
+              },
+            ],
             'x-reactions': ['{{useAsyncDataSource(getTypes)}}'],
           },
           target: {
@@ -179,6 +177,12 @@ const Save = (props: Props) => {
                 },
               },
             },
+            'x-validator': [
+              {
+                required: true,
+                message: '请选择被关联方',
+              },
+            ],
             required: true,
           },
           description: {