Ver código fonte

feat(merge): merge xyh

lind 3 anos atrás
pai
commit
048b2c19ca
57 arquivos alterados com 2989 adições e 607 exclusões
  1. 3 1
      src/components/ProTableCard/CardItems/protocol.tsx
  2. 2 2
      src/components/ProTableCard/CardItems/scene.tsx
  3. 1 1
      src/pages/device/Instance/Detail/Config/index.tsx
  4. 23 5
      src/pages/device/Instance/Detail/Diagnose/Message/index.tsx
  5. 15 10
      src/pages/device/Instance/Detail/Diagnose/Status/DiagnosticAdvice.tsx
  6. 5 3
      src/pages/device/Instance/Detail/Diagnose/Status/ManualInspection.tsx
  7. 145 131
      src/pages/device/Instance/Detail/Diagnose/Status/index.tsx
  8. 4 4
      src/pages/device/Instance/Detail/Diagnose/Status/model.ts
  9. 7 4
      src/pages/device/Instance/Detail/Diagnose/index.tsx
  10. 30 7
      src/pages/device/Instance/Detail/Functions/form.tsx
  11. 3 3
      src/pages/device/Instance/Detail/MetadataMap/EditableTable/index.tsx
  12. 2 2
      src/pages/device/Instance/Detail/Reation/Edit.tsx
  13. 53 25
      src/pages/device/Instance/Detail/Running/Property/Indicators.tsx
  14. 54 30
      src/pages/device/Instance/Detail/index.tsx
  15. 2 1
      src/pages/device/Instance/Import/index.tsx
  16. 19 9
      src/pages/device/Instance/Save/index.tsx
  17. 1 0
      src/pages/device/Instance/index.tsx
  18. 14 4
      src/pages/device/Instance/service.ts
  19. 19 12
      src/pages/device/Product/Detail/Access/index.tsx
  20. 4 2
      src/pages/link/AccessConfig/Detail/Provider/index.less
  21. 2 2
      src/pages/link/AccessConfig/Detail/Provider/index.tsx
  22. 12 2
      src/pages/link/Protocol/index.tsx
  23. 1 1
      src/pages/link/Protocol/save/index.tsx
  24. 139 0
      src/pages/rule-engine/Scene/Save/action/VariableItems/builtIn.tsx
  25. 1 0
      src/pages/rule-engine/Scene/Save/action/VariableItems/email.tsx
  26. 4 0
      src/pages/rule-engine/Scene/Save/action/VariableItems/index.ts
  27. 63 0
      src/pages/rule-engine/Scene/Save/action/VariableItems/org.tsx
  28. 50 0
      src/pages/rule-engine/Scene/Save/action/VariableItems/tag.tsx
  29. 232 0
      src/pages/rule-engine/Scene/Save/action/VariableItems/user.tsx
  30. 70 47
      src/pages/rule-engine/Scene/Save/action/action.tsx
  31. 152 0
      src/pages/rule-engine/Scene/Save/action/device/WriteProperty/index.tsx
  32. 179 0
      src/pages/rule-engine/Scene/Save/action/device/deviceModal.tsx
  33. 151 0
      src/pages/rule-engine/Scene/Save/action/device/functionCall.tsx
  34. 182 0
      src/pages/rule-engine/Scene/Save/action/device/index.tsx
  35. 24 0
      src/pages/rule-engine/Scene/Save/action/device/readProperty.tsx
  36. 13 0
      src/pages/rule-engine/Scene/Save/action/device/service.ts
  37. 0 0
      src/pages/rule-engine/Scene/Save/action/device/sourceItem.tsx
  38. 263 0
      src/pages/rule-engine/Scene/Save/action/device/tagModal.tsx
  39. 41 133
      src/pages/rule-engine/Scene/Save/action/messageContent.tsx
  40. 40 0
      src/pages/rule-engine/Scene/Save/action/service.ts
  41. 28 0
      src/pages/rule-engine/Scene/Save/components/DatePickerFormat/index.tsx
  42. 53 0
      src/pages/rule-engine/Scene/Save/components/InputNumber.tsx
  43. 2 2
      src/pages/rule-engine/Scene/Save/components/InputUpload/index.tsx
  44. 7 2
      src/pages/rule-engine/Scene/Save/components/TimeSelect/index.tsx
  45. 179 34
      src/pages/rule-engine/Scene/Save/components/TimingTrigger/index.tsx
  46. 5 0
      src/pages/rule-engine/Scene/Save/components/TriggerWay/index.tsx
  47. 2 0
      src/pages/rule-engine/Scene/Save/components/index.ts
  48. 215 94
      src/pages/rule-engine/Scene/Save/index.tsx
  49. 262 6
      src/pages/rule-engine/Scene/Save/trigger/index.tsx
  50. 81 0
      src/pages/rule-engine/Scene/Save/trigger/operation.tsx
  51. 27 0
      src/pages/rule-engine/Scene/Save/trigger/service.ts
  52. 1 1
      src/pages/rule-engine/Scene/TriggerTerm/index.tsx
  53. 49 24
      src/pages/rule-engine/Scene/index.tsx
  54. 3 1
      src/pages/rule-engine/Scene/service.ts
  55. 1 1
      src/pages/system/Department/Assets/product/bind.tsx
  56. 43 1
      src/pages/system/Relationship/Save/index.tsx
  57. 6 0
      src/pages/system/Relationship/service.ts

+ 3 - 1
src/components/ProTableCard/CardItems/protocol.tsx

@@ -32,7 +32,9 @@ export default (props: ProcotolCardProps) => {
         </div>
         <div className={'card-item-body'}>
           <div className={'card-item-header'}>
-            <span className={'card-item-header-name ellipsis'}>{props.name}</span>
+            <Tooltip title={props.name}>
+              <span className={'card-item-header-name ellipsis'}>{props.name}</span>
+            </Tooltip>
           </div>
           <Row gutter={24}>
             <Col span={12}>

+ 2 - 2
src/components/ProTableCard/CardItems/scene.tsx

@@ -19,8 +19,8 @@ export default (props: DeviceCardProps) => {
       status={props.state.value}
       statusText={props.state.text}
       statusNames={{
-        online: StatusColorEnum.processing,
-        offline: StatusColorEnum.error,
+        started: StatusColorEnum.processing,
+        disable: StatusColorEnum.error,
         notActive: StatusColorEnum.warning,
       }}
     >

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

@@ -143,7 +143,7 @@ const Config = () => {
           )}
         </Space>
       </div>
-      <div style={{ paddingLeft: 10 }}>
+      <div>
         {(metadata || []).map((i) => (
           <Descriptions size="small" column={3} key={i.name} bordered title={<h4>{i.name}</h4>}>
             {(i?.properties || []).map((item: any) => (

+ 23 - 5
src/pages/device/Instance/Detail/Diagnose/Message/index.tsx

@@ -31,6 +31,7 @@ const DatePicker1: any = DatePicker;
 const Message = (props: Props) => {
   const [subscribeTopic] = useSendWebsocketMessage();
   const [dialogList, setDialogList] = useState<any[]>([]);
+  const [tempList, setTempList] = useState<any[]>([]);
   const [logList, setLogList] = useState<any[]>([]);
   const [type, setType] = useState<'property' | 'function'>('function');
   const [input, setInput] = useState<any>({});
@@ -48,11 +49,6 @@ const Message = (props: Props) => {
     subscribeTopic!(id, topic, {})
       ?.pipe(map((res) => res.payload))
       .subscribe((payload: any) => {
-        if (payload.error) {
-          props.onChange(!payload.upstream ? 'down-error' : 'up-error');
-        } else {
-          props.onChange(!payload.upstream ? 'down-success' : 'up-success');
-        }
         if (payload.type === 'log') {
           logList.push({
             key: randomString(),
@@ -60,6 +56,26 @@ const Message = (props: Props) => {
           });
           setLogList([...logList]);
         } else {
+          tempList.push({
+            key: randomString(),
+            ...payload,
+          });
+          const flag = [...tempList]
+            .filter(
+              (i) =>
+                i.traceId === payload.traceId &&
+                (payload.downstream === i.downstream || payload.upstream === i.upstream),
+            )
+            .every((item) => {
+              return !item.error;
+            });
+          if (!flag) {
+            props.onChange(!payload.upstream ? 'down-error' : 'up-error');
+          } else {
+            props.onChange(!payload.upstream ? 'down-success' : 'up-success');
+          }
+          setTempList([...tempList]);
+          Store.set('temp', tempList);
           const t = dialogList.find(
             (item) =>
               item.traceId === payload.traceId &&
@@ -153,6 +169,8 @@ const Message = (props: Props) => {
     subscribeLog();
     const arr = Store.get('diagnose') || [];
     setDialogList(arr);
+    const temp = Store.get('temp') || [];
+    setTempList(temp);
   }, []);
 
   const form = createForm({

+ 15 - 10
src/pages/device/Instance/Detail/Diagnose/Status/DiagnosticAdvice.tsx

@@ -23,8 +23,7 @@ const DiagnosticAdvice = (props: Props) => {
     const tab: any = window.open(`${origin}/#${url}?key=access`);
     tab!.onTabSaveSuccess = (value: any) => {
       if (value) {
-        // diagnoseConfig();
-        // 没有权限怎么展示
+        props.close();
       }
     };
   };
@@ -35,6 +34,9 @@ const DiagnosticAdvice = (props: Props) => {
       onCancel={() => {
         props.close();
       }}
+      onOk={() => {
+        props.close();
+      }}
       width={700}
       visible
     >
@@ -49,7 +51,7 @@ const DiagnosticAdvice = (props: Props) => {
               status="default"
               text={
                 <span>
-                  产品-${item.name}规则可能有加密处理,请认真查看
+                  产品-{item.name}规则可能有加密处理,请认真查看
                   <a
                     onClick={() => {
                       jumpUrl();
@@ -69,7 +71,7 @@ const DiagnosticAdvice = (props: Props) => {
               status="default"
               text={
                 <span>
-                  设备-${item.name}规则可能有加密处理,请认真查看
+                  设备-{item.name}规则可能有加密处理,请认真查看
                   <a
                     onClick={() => {
                       jumpUrl();
@@ -83,7 +85,7 @@ const DiagnosticAdvice = (props: Props) => {
             />
           </div>
         ))}
-        {!!data.provider && (
+        {!!data?.provider && (
           <div>
             {data.routes.length > 0 ? (
               <div className={styles.infoItem}>
@@ -99,9 +101,9 @@ const DiagnosticAdvice = (props: Props) => {
                       >
                         设备接入配置
                       </a>
-                      中${nameMap.get(data.provider)}
-                      信息,任意上报一条数据(无设备接入配置查看权限时:请联系管理员根据设备接入配置中$
-                      {URL}信息,任意上报一条数据)。 变量说明:${nameMap.get(data.provider)}
+                      中{nameMap.get(data.provider)}
+                      信息,任意上报一条数据(无设备接入配置查看权限时:请联系管理员根据设备接入配置中
+                      {URL}信息,任意上报一条数据)。 变量说明:{nameMap.get(data.provider)}
                       变量根据网关详情中provider类型判断。
                     </span>
                   }
@@ -113,8 +115,8 @@ const DiagnosticAdvice = (props: Props) => {
                   status="default"
                   text={
                     <span>
-                      请联系管理员提供${nameMap.get(data.provider)}
-                      信息,并根据URL信息任意上报一条数据 变量说明:${nameMap.get(data.provider)}
+                      请联系管理员提供{nameMap.get(data.provider)}
+                      信息,并根据URL信息任意上报一条数据 变量说明:{nameMap.get(data.provider)}
                       变量根据网关详情中provider类型判断。
                     </span>
                   }
@@ -123,6 +125,9 @@ const DiagnosticAdvice = (props: Props) => {
             )}
           </div>
         )}
+        <div className={styles.infoItem}>
+          <Badge status="default" text={'请检查设备是否已开机'} />
+        </div>
       </div>
     </Modal>
   );

+ 5 - 3
src/pages/device/Instance/Detail/Diagnose/Status/ManualInspection.tsx

@@ -85,9 +85,11 @@ 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 key={item?.property}>
+          <span>{item?.name}</span>:{' '}
+          <span>
+            {data?.check && data?.check[item?.property] ? data?.check[item?.property] : '--'}
+          </span>
         </div>
       ))}
     </div>

+ 145 - 131
src/pages/device/Instance/Detail/Diagnose/Status/index.tsx

@@ -35,25 +35,25 @@ const Status = observer((props: Props) => {
       key: 'config',
       name: '设备接入配置',
       data: 'config',
-      desc: '诊断设备接入配置是否正确,配置错误将导致连接失败',
+      desc: '诊断设备接入配置是否正确,配置错误将导致连接失败',
     },
     {
       key: 'network',
       name: '网络信息',
       data: 'network',
-      desc: '诊断网络组件配置是否正确,配置错误将导致连接失败',
+      desc: '诊断网络组件配置是否正确,配置错误将导致连接失败',
     },
     {
       key: 'product',
       name: '产品状态',
       data: 'product',
-      desc: '诊断产品状态是否已发布,未发布的状态将导致连接失败',
+      desc: '诊断产品状态是否已发布,未发布的状态将导致连接失败',
     },
     {
       key: 'device',
       name: '设备状态',
       data: 'device',
-      desc: '诊断设备状态是否已启用,未启用的状态将导致连接失败',
+      desc: '诊断设备状态是否已启用,未启用的状态将导致连接失败',
     },
     {
       key: 'device-access',
@@ -74,12 +74,18 @@ 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 accessPermission = PermissionButton.usePermission('link/AccessConfig').permission;
 
   const [diagnoseVisible, setDiagnoseVisible] = useState<boolean>(false);
   const [artificialVisible, setArtificialVisible] = useState<boolean>(false);
   const [diagnoseData, setDiagnoseData] = useState<any>({});
   const [artificiaData, setArtificiaData] = useState<any>({});
 
+  const [productTemp, setProductTemp] = useState<any[]>([]);
+  const [deviceTemp, setDeviceTemp] = useState<any[]>([]);
+  const [gatewayTemp, setGatewayTemp] = useState<any>({});
+  const [productItem, setProductItem] = useState<any>({});
+
   const getDetail = (id: string) => {
     service.detail(id).then((response) => {
       InstanceModel.detail = response?.result;
@@ -91,6 +97,7 @@ const Status = observer((props: Props) => {
       let data: any = {};
       if (InstanceModel.detail.state?.value === 'online' || !!InstanceModel.detail?.protocol) {
         data = { status: 'success', text: '正常', info: null };
+        DiagnoseStatusModel.status = { ...DiagnoseStatusModel.status };
       } else {
         data = {
           status: 'warning',
@@ -142,6 +149,7 @@ const Status = observer((props: Props) => {
       if (InstanceModel.detail.state?.value === 'online') {
         data = { status: 'success', text: '正常', info: null };
         DiagnoseStatusModel.status.network = data;
+        DiagnoseStatusModel.status = { ...DiagnoseStatusModel.status };
         setTimeout(
           () =>
             resolve({
@@ -154,6 +162,7 @@ const Status = observer((props: Props) => {
       } else {
         service.queryProductState(InstanceModel.detail?.productId || '').then((resp) => {
           if (resp.status === 200) {
+            setProductItem(resp.result);
             if (resp.result.accessId) {
               service.queryGatewayState(resp.result.accessId).then((response: any) => {
                 if (response.status === 200) {
@@ -186,62 +195,65 @@ const Status = observer((props: Props) => {
                         </div>
                       ),
                     };
-                  } else if (gatewayList.includes(provider) && health === -1) {
-                    data = {
-                      status: 'error',
-                      text: '网络异常',
-                      info: (
-                        <div>
-                          <div className={styles.infoItem}>
-                            <Badge status="default" text={<span>请联系开发人员排查问题</span>} />
+                  } else if (health === -1) {
+                    if (gatewayList.includes(provider)) {
+                      data = {
+                        status: 'error',
+                        text: '网络异常',
+                        info: (
+                          <div>
+                            <div className={styles.infoItem}>
+                              <Badge
+                                status="default"
+                                text={
+                                  networkPermission.action ? (
+                                    <span>
+                                      网络组件未启用, 请
+                                      <Popconfirm
+                                        title="确认启用"
+                                        onConfirm={async () => {
+                                          const res = await service.startNetwork(
+                                            resp.result?.channelId,
+                                          );
+                                          if (res.status === 200) {
+                                            message.success('操作成功!');
+                                            DiagnoseStatusModel.status.network = {
+                                              status: 'success',
+                                              text: '正常',
+                                              info: null,
+                                            };
+                                          }
+                                        }}
+                                      >
+                                        <a>启用</a>
+                                      </Popconfirm>
+                                      网络组件
+                                    </span>
+                                  ) : (
+                                    '网络组件未启用,请联系管理员'
+                                  )
+                                }
+                              />
+                            </div>
                           </div>
-                        </div>
-                      ),
-                    };
-                  } else {
-                    data = {
-                      status: 'error',
-                      text: '网络异常',
-                      info: (
-                        <div>
-                          <div className={styles.infoItem}>
-                            <Badge
-                              status="default"
-                              text={
-                                networkPermission.action ? (
-                                  <span>
-                                    网络组件未启用, 请
-                                    <Popconfirm
-                                      title="确认启用"
-                                      onConfirm={async () => {
-                                        const res = await service.startNetwork(
-                                          resp.result?.channelId,
-                                        );
-                                        if (res.status === 200) {
-                                          message.success('操作成功!');
-                                          DiagnoseStatusModel.status.network = {
-                                            status: 'success',
-                                            text: '正常',
-                                            info: null,
-                                          };
-                                        }
-                                      }}
-                                    >
-                                      <a>启用</a>
-                                    </Popconfirm>
-                                    网络组件
-                                  </span>
-                                ) : (
-                                  '网络组件未启用,请联系管理员'
-                                )
-                              }
-                            />
+                        ),
+                      };
+                    } else {
+                      data = {
+                        status: 'error',
+                        text: '网络异常',
+                        info: (
+                          <div>
+                            <div className={styles.infoItem}>
+                              <Badge status="default" text={<span>请联系开发人员排查问题</span>} />
+                            </div>
                           </div>
-                        </div>
-                      ),
-                    };
+                        ),
+                      };
+                    }
                   }
                   DiagnoseStatusModel.status.network = data;
+                  DiagnoseStatusModel.status = { ...DiagnoseStatusModel.status };
                   setTimeout(
                     () =>
                       resolve({
@@ -330,6 +342,7 @@ const Status = observer((props: Props) => {
                                 text: '正常',
                                 info: null,
                               };
+                              DiagnoseStatusModel.status = { ...DiagnoseStatusModel.status };
                             }
                           }}
                         >
@@ -338,7 +351,7 @@ const Status = observer((props: Props) => {
                         产品
                       </span>
                     ) : (
-                      '无产品发布权限时:产品未发布。请联系管理员处理。'
+                      '无产品发布权限时:产品未发布,请联系管理员处理'
                     )
                   }
                 />
@@ -384,6 +397,7 @@ const Status = observer((props: Props) => {
                               info: null,
                             };
                             getDetail(InstanceModel.detail?.id || '');
+                            DiagnoseStatusModel.status = { ...DiagnoseStatusModel.status };
                           }
                         }}
                       >
@@ -392,7 +406,7 @@ const Status = observer((props: Props) => {
                       设备
                     </span>
                   ) : (
-                    '设备未启用。请联系管理员处理。'
+                    '设备未启用,请联系管理员处理'
                   )
                 }
               />
@@ -414,6 +428,7 @@ const Status = observer((props: Props) => {
       } else {
         service.queryProductConfig(proItem.id).then((resp) => {
           if (resp.status === 200) {
+            setProductTemp(resp?.result);
             if (resp.result.length > 0) {
               resp.result.map((item: any, index: number) => {
                 let data: any = {};
@@ -475,6 +490,7 @@ const Status = observer((props: Props) => {
       } else {
         service.queryDeviceConfig(InstanceModel.detail?.id || '').then((resp) => {
           if (resp.status === 200) {
+            setDeviceTemp(resp.result);
             if (resp.result.length > 0) {
               resp.result.map((item: any, index: number) => {
                 let data: any = {};
@@ -544,26 +560,31 @@ const Status = observer((props: Props) => {
                 <Badge
                   status="default"
                   text={
-                    <span>
-                      设备接入网关未启用,请
-                      <Popconfirm
-                        title="确认启用"
-                        onConfirm={async () => {
-                          const resp = await service.startGateway(gateway?.id || '');
-                          if (resp.status === 200) {
-                            message.success('操作成功!');
-                            DiagnoseStatusModel.status.deviceAccess = {
-                              status: 'success',
-                              text: '正常',
-                              info: null,
-                            };
-                          }
-                        }}
-                      >
-                        <a>启用</a>
-                      </Popconfirm>
-                      设备接入网关
-                    </span>
+                    accessPermission.action ? (
+                      <span>
+                        设备接入网关未启用,请
+                        <Popconfirm
+                          title="确认启用"
+                          onConfirm={async () => {
+                            const resp = await service.startGateway(gateway?.id || '');
+                            if (resp.status === 200) {
+                              message.success('操作成功!');
+                              DiagnoseStatusModel.status.deviceAccess = {
+                                status: 'success',
+                                text: '正常',
+                                info: null,
+                              };
+                              DiagnoseStatusModel.status = { ...DiagnoseStatusModel.status };
+                            }
+                          }}
+                        >
+                          <a>启用</a>
+                        </Popconfirm>
+                        设备接入网关
+                      </span>
+                    ) : (
+                      '设备接入网关未启用。请联系管理员处理'
+                    )
                   }
                 />
               </div>
@@ -607,68 +628,26 @@ const Status = observer((props: Props) => {
 
     let product: any = null;
     let gateway: any = null;
-    let productauth: any = null;
-    let deviceauth: any = null;
     diagnoseConfig()
       .then(() => diagnoseNetwork())
       .then((resp: any) => {
         product = resp?.product;
         gateway = resp?.gatewayDetail;
+        setGatewayTemp(resp?.gatewayDetail);
         diagnoseProduct(product)
           .then(() => diagnoseDevice())
           .then(() => diagnoseProductAuthConfig(product))
-          .then((res) => {
-            productauth = res;
-            diagnoseDeviceAuthConfig().then((dt) => {
-              deviceauth = dt;
+          .then(() => {
+            diagnoseDeviceAuthConfig().then(() => {
               diagnoseDeviceAccess(gateway).then(() => {
-                if (InstanceModel.detail.state?.value === 'online') {
-                  const a = Object.keys(DiagnoseStatusModel.status).find((item: any) => {
-                    return item.status !== 'success';
-                  });
-                  if (!!a) {
-                    Store.set('diagnose-status', {
-                      list: DiagnoseStatusModel.list,
-                      status: DiagnoseStatusModel.status,
-                    });
-                    props.onChange('success');
-                  } else {
-                    props.onChange('error');
-                  }
+                if (InstanceModel.detail.state?.value !== 'online') {
+                  props.onChange('error');
                 } else {
-                  const data = { ...DiagnoseStatusModel.status };
-                  const flag = Object.keys(data).find((item: any) => {
-                    return item.status !== 'success';
+                  Store.set('diagnose-status', {
+                    list: DiagnoseStatusModel.list,
+                    status: DiagnoseStatusModel.status,
                   });
-                  if (!flag) {
-                    // 展示诊断建议
-                    if (
-                      gateway.provider !== 'mqtt-server-gateway' &&
-                      gatewayList.includes(gateway.provider)
-                    ) {
-                      service
-                        .queryProcotolDetail(gateway.provider, gateway.transport)
-                        .then((resp1) => {
-                          setDiagnoseData({
-                            product: productauth,
-                            device: deviceauth,
-                            id: product.id,
-                            provider: gateway.provider,
-                            routes: resp1.result?.routes || [],
-                          });
-                          setDiagnoseVisible(true);
-                        });
-                    } else {
-                      setDiagnoseData({
-                        product: productauth,
-                        device: deviceauth,
-                        id: product.id,
-                        provider: '',
-                        routes: [],
-                      });
-                      setDiagnoseVisible(true);
-                    }
-                  }
+                  props.onChange('success');
                 }
               });
             });
@@ -689,6 +668,40 @@ const Status = observer((props: Props) => {
     }
   }, [devicePermission]);
 
+  useEffect(() => {
+    const data = { ...DiagnoseStatusModel.status };
+    const flag = Object.keys(data).every((item: any) => {
+      return data[item]?.status === 'success';
+    });
+    if (flag && InstanceModel.detail.state?.value !== 'online') {
+      // 展示诊断建议
+      if (
+        gatewayTemp.provider !== 'mqtt-server-gateway' &&
+        gatewayList.includes(gatewayTemp.provider)
+      ) {
+        service.queryProcotolDetail(gatewayTemp.provider, gatewayTemp.transport).then((resp1) => {
+          setDiagnoseData({
+            product: productTemp,
+            device: deviceTemp,
+            id: productItem.id,
+            provider: gatewayTemp.provider,
+            routes: resp1.result?.routes || [],
+          });
+          setDiagnoseVisible(true);
+        });
+      } else {
+        setDiagnoseData({
+          product: productTemp,
+          device: deviceTemp,
+          id: productItem.i,
+          provider: gatewayTemp?.provider,
+          routes: [],
+        });
+        setDiagnoseVisible(true);
+      }
+    }
+  }, [DiagnoseStatusModel.status]);
+
   return (
     <Row gutter={24}>
       <Col span={16}>
@@ -762,6 +775,7 @@ const Status = observer((props: Props) => {
                 text: '正常',
                 info: null,
               };
+              DiagnoseStatusModel.status = { ...DiagnoseStatusModel.status };
             } else {
               DiagnoseStatusModel.status[params.data.key] = {
                 status: 'error',
@@ -783,7 +797,8 @@ const Status = observer((props: Props) => {
                               const tab: any = window.open(`${origin}/#${url}?key=access`);
                               tab!.onTabSaveSuccess = (value: any) => {
                                 if (value) {
-                                  diagnoseConfig();
+                                  getDetail(InstanceModel.detail?.id || '');
+                                  handleSearch();
                                 }
                               };
                             }}
@@ -799,7 +814,6 @@ const Status = observer((props: Props) => {
                           >
                             重新比对
                           </a>
-                          。
                         </span>
                       }
                     />

+ 4 - 4
src/pages/device/Instance/Detail/Diagnose/Status/model.ts

@@ -64,25 +64,25 @@ export const DiagnoseStatusModel = model<{
       key: 'config',
       name: '设备接入配置',
       data: 'config',
-      desc: '诊断设备接入配置是否正确,配置错误将导致连接失败',
+      desc: '诊断设备接入配置是否正确,配置错误将导致连接失败',
     },
     {
       key: 'network',
       name: '网络信息',
       data: 'network',
-      desc: '诊断网络组件配置是否正确,配置错误将导致连接失败',
+      desc: '诊断网络组件配置是否正确,配置错误将导致连接失败',
     },
     {
       key: 'product',
       name: '产品状态',
       data: 'product',
-      desc: '诊断产品状态是否已发布,未发布的状态将导致连接失败',
+      desc: '诊断产品状态是否已发布,未发布的状态将导致连接失败',
     },
     {
       key: 'device',
       name: '设备状态',
       data: 'device',
-      desc: '诊断设备状态是否已启用,未启用的状态将导致连接失败',
+      desc: '诊断设备状态是否已启用,未启用的状态将导致连接失败',
     },
   ],
 });

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

@@ -1,4 +1,4 @@
-import { Badge, Card, Col, Row } from 'antd';
+import { Badge, Card, Col, Row, Tooltip } from 'antd';
 import type { ReactNode } from 'react';
 import { useEffect, useState } from 'react';
 import Message from './Message';
@@ -87,9 +87,12 @@ const Diagnose = () => {
             style={{ fontWeight: 400 }}
           >
             {message === 'disabled' ? (
-              <span style={{ color: statusColor.get(message) }}>
-                <Badge color={statusColor.get(message)} /> 连接中
-              </span>
+              <Tooltip title={'设备未上线时消息通信功不能使用'}>
+                <span style={{ color: statusColor.get(message) }}>
+                  <Badge color={statusColor.get(message)} />
+                  {status === 's-error' || status === 'waiting' ? '等待设备连接' : '连接中'}
+                </span>
+              </Tooltip>
             ) : (
               <>
                 <div>

+ 30 - 7
src/pages/device/Instance/Detail/Functions/form.tsx

@@ -27,8 +27,20 @@ export default (props: FunctionProps) => {
   const formRef = useRef<ProFormInstance<any>>();
   const [editableKeys, setEditableRowKeys] = useState<React.Key[]>([]);
 
-  const getItemNode = (type: string) => {
+  const getItemNode = (record: any) => {
+    const type = record.type;
+    const name = record.name;
+
     switch (type) {
+      case 'enum':
+        return (
+          <Select
+            style={{ width: '100%', textAlign: 'left' }}
+            options={record.options}
+            fieldNames={{ label: 'text', value: 'value' }}
+            placeholder={'请选择' + name}
+          />
+        );
       case 'boolean':
         return (
           <Select
@@ -37,19 +49,28 @@ export default (props: FunctionProps) => {
               { label: 'true', value: true },
               { label: 'false', value: false },
             ]}
-            placeholder={'请选择'}
+            placeholder={'请选择' + name}
           />
         );
       case 'int':
       case 'long':
       case 'float':
       case 'double':
-        return <InputNumber style={{ width: '100%' }} placeholder={'请输入'} />;
+        return <InputNumber style={{ width: '100%' }} placeholder={'请输入' + name} />;
       case 'date':
-        // @ts-ignore
-        return <DatePicker style={{ width: '100%' }} />;
+        return (
+          <>
+            {
+              // @ts-ignore
+              <DatePicker
+                format={record.format || 'YYYY-MM-DD HH:mm:ss'}
+                style={{ width: '100%' }}
+              />
+            }
+          </>
+        );
       default:
-        return <Input placeholder={'请输入'} />;
+        return <Input placeholder={'请输入' + name} />;
     }
   };
 
@@ -75,7 +96,7 @@ export default (props: FunctionProps) => {
       align: 'center',
       width: 260,
       renderFormItem: (_, row: any) => {
-        return getItemNode(row.record.type);
+        return getItemNode(row.record);
       },
     },
   ];
@@ -89,6 +110,8 @@ export default (props: FunctionProps) => {
         id: datum.id,
         name: datum.name,
         type: datum.valueType ? datum.valueType.type : '-',
+        format: datum.valueType ? datum.valueType.format : undefined,
+        options: datum.valueType ? datum.valueType.elements : undefined,
         value: undefined,
       });
     }

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

@@ -56,7 +56,7 @@ const EditableCell = ({
 
   useEffect(() => {
     if (record) {
-      form.setFieldsValue({ [dataIndex]: record[dataIndex] });
+      form.setFieldsValue({ [dataIndex]: record.customMapping ? record[dataIndex] : '' });
       setTemp(properties.find((i) => i.id === record.originalId));
     }
   }, [record]);
@@ -74,7 +74,7 @@ const EditableCell = ({
             (option?.children || '').toLowerCase()?.indexOf(input.toLowerCase()) >= 0
           }
         >
-          <Select.Option value={record.metadataId}>使用物模型属性</Select.Option>
+          <Select.Option value={''}>使用物模型属性</Select.Option>
           {record.originalId !== record.metadataId && (
             <Select.Option value={record.originalId}>
               {temp?.name}({temp?.id})
@@ -212,7 +212,7 @@ const EditableTable = (props: Props) => {
         {
           metadataType: 'property',
           metadataId: row.metadataId,
-          originalId: row.metadataId !== row.originalId ? row.originalId : '',
+          originalId: row.originalId,
           others: {},
         },
       ]);

+ 2 - 2
src/pages/device/Instance/Detail/Reation/Edit.tsx

@@ -3,7 +3,7 @@ import { createForm } from '@formily/core';
 import { createSchemaField } from '@formily/react';
 import { InstanceModel, service } from '@/pages/device/Instance';
 import type { ISchema } from '@formily/json-schema';
-import { Form, FormGrid, FormItem, Select, PreviewText } from '@formily/antd';
+import { Form, FormGrid, FormItem, PreviewText, Select } from '@formily/antd';
 import { useParams } from 'umi';
 import { Button, Drawer, message, Space } from 'antd';
 import { action } from '@formily/reactive';
@@ -63,7 +63,7 @@ const Edit = (props: Props) => {
         'x-decorator': 'FormItem',
         'x-component': 'Select',
         'x-component-props': {
-          placeholder: '请选择关联方',
+          placeholder: `请选择${item.relationName}`,
           showSearch: true,
           showArrow: true,
           mode: 'multiple',

+ 53 - 25
src/pages/device/Instance/Detail/Running/Property/Indicators.tsx

@@ -10,9 +10,8 @@ import {
   NumberPicker,
   Select,
 } from '@formily/antd';
-import { createForm } from '@formily/core';
+import { createForm, onFieldReact } from '@formily/core';
 import { createSchemaField } from '@formily/react';
-
 import type { PropertyMetadata } from '@/pages/device/Product/typings';
 import { useEffect, useState } from 'react';
 import { InstanceModel, service } from '@/pages/device/Instance';
@@ -42,6 +41,31 @@ const Indicators = (props: Props) => {
     initialValues: {
       metrics: metrics,
     },
+    effects: () => {
+      onFieldReact('metrics.*.*', (field, form1) => {
+        const type = data.valueType?.type;
+        form1.setFieldState('metrics.*.space.value.*', (state) => {
+          state.componentType = componentMap[type || ''] || 'Input';
+          if (type === 'date') {
+            state.componentProps = {
+              showTime: true,
+            };
+          } else if (type === 'boolean') {
+            state.componentType = 'Select';
+            state.dataSource = [
+              {
+                label: data.valueType?.trueText,
+                value: String(data.valueType?.trueValue),
+              },
+              {
+                label: data.valueType?.falseText,
+                value: String(data.valueType?.falseValue),
+              },
+            ];
+          }
+        });
+      });
+    },
   });
 
   const SchemaField = createSchemaField({
@@ -106,19 +130,6 @@ const Indicators = (props: Props) => {
                       dependencies: ['..range', data.valueType?.type],
                       fulfill: {
                         state: {
-                          dataSource:
-                            data.valueType?.type === 'boolean'
-                              ? [
-                                  {
-                                    label: data.valueType?.trueText,
-                                    value: data.valueType?.trueValue,
-                                  },
-                                  {
-                                    label: data.valueType?.falseText,
-                                    value: data.valueType?.falseValue,
-                                  },
-                                ]
-                              : [],
                           decoratorProps: {
                             gridSpan: '{{!!$deps[0]?5:$deps[1]==="boolean"?12:10}}',
                           },
@@ -163,17 +174,32 @@ const Indicators = (props: Props) => {
     },
   };
 
-  console.log(data);
-  console.log(InstanceModel.detail);
-
   useEffect(() => {
     if (InstanceModel.detail.id && data.id) {
       service.queryMetric(InstanceModel.detail.id || '', data.id || '').then((resp) => {
         if (resp.status === 200) {
           if ((resp?.result || []).length > 0) {
-            setMetrics(resp.result);
+            const list = resp.result.map((item: any) => {
+              return {
+                ...item,
+                value: item.value.split(','),
+              };
+            });
+            setMetrics(list);
           } else {
-            setMetrics(data.expands?.metrics || []);
+            const type = data.valueType?.type;
+            if (type === 'boolean') {
+              const list = data.expands?.metrics.map((item: any) => {
+                const value = (item?.value || {}).map((i: any) => String(i)) || {};
+                return {
+                  ...item,
+                  value,
+                };
+              });
+              setMetrics(list || []);
+            } else {
+              setMetrics(data.expands?.metrics || []);
+            }
           }
         }
       });
@@ -188,11 +214,13 @@ const Indicators = (props: Props) => {
       width={600}
       onOk={async () => {
         const params = (await form.submit()) as any;
-        const resp = await service.saveMetric(
-          InstanceModel.detail.id || '',
-          data.id || '',
-          params.metrics,
-        );
+        const list = (params?.metrics || []).map((item: any) => {
+          return {
+            ...item,
+            value: item.value.join(','),
+          };
+        });
+        const resp = await service.saveMetric(InstanceModel.detail.id || '', data.id || '', list);
         if (resp.status === 200) {
           message.success('操作成功!');
           props.onCancel();

+ 54 - 30
src/pages/device/Instance/Detail/index.tsx

@@ -263,36 +263,60 @@ const InstanceDetail = observer(() => {
           <Divider type="vertical" />
           <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>
+            {InstanceModel.detail?.state?.value === 'notActive' && (
+              <PermissionButton
+                type={'link'}
+                key={'state'}
+                popConfirm={{
+                  title: '确认启用设备',
+                  onConfirm: async () => {
+                    const resp = await service.deployDevice(params.id);
+                    if (resp.status === 200) {
+                      message.success(
+                        intl.formatMessage({
+                          id: 'pages.data.option.success',
+                          defaultMessage: '操作成功!',
+                        }),
+                      );
+                      getDetail(params.id);
+                    }
+                  },
+                }}
+                isPermission={permission.action}
+                tooltip={{
+                  title: '启用设备',
+                }}
+              >
+                启用设备
+              </PermissionButton>
+            )}
+            {InstanceModel.detail?.state?.value === 'online' && (
+              <PermissionButton
+                type={'link'}
+                key={'state'}
+                popConfirm={{
+                  title: '确认断开连接',
+                  onConfirm: async () => {
+                    const resp = await service.disconnectDevice(params.id);
+                    if (resp.status === 200) {
+                      message.success(
+                        intl.formatMessage({
+                          id: 'pages.data.option.success',
+                          defaultMessage: '操作成功!',
+                        }),
+                      );
+                      getDetail(params.id);
+                    }
+                  },
+                }}
+                isPermission={permission.action}
+                tooltip={{
+                  title: '断开连接',
+                }}
+              >
+                断开连接
+              </PermissionButton>
+            )}
           </Space>
         </>
       }

+ 2 - 1
src/pages/device/Instance/Import/index.tsx

@@ -12,6 +12,7 @@ import SystemConst from '@/utils/const';
 import Token from '@/utils/token';
 import { EventSourcePolyfill } from 'event-source-polyfill';
 import { downloadFile } from '@/utils/util';
+import encodeQuery from '@/utils/encodeQuery';
 
 interface Props {
   visible: boolean;
@@ -176,7 +177,7 @@ const Import = (props: Props) => {
   });
 
   useEffect(() => {
-    service.getProductList({ paging: false }).then((resp) => {
+    service.getProductList(encodeQuery({ paging: false, terms: { state: 1 } })).then((resp) => {
       if (resp.status === 200) {
         const list = resp.result.map((item: { name: any; id: any }) => ({
           label: item.name,

+ 19 - 9
src/pages/device/Instance/Save/index.tsx

@@ -5,6 +5,7 @@ import { useEffect, useState } from 'react';
 import { useIntl } from '@@/plugin-locale/localeExports';
 import { UploadImage } from '@/components';
 import { debounce } from 'lodash';
+import encodeQuery from '@/utils/encodeQuery';
 
 interface Props {
   visible: boolean;
@@ -31,15 +32,24 @@ const Save = (props: Props) => {
   const intl = useIntl();
 
   useEffect(() => {
-    service.getProductList({ paging: false }).then((resp: any) => {
-      if (resp.status === 200) {
-        const list = resp.result.map((item: { name: any; id: any }) => ({
-          label: item.name,
-          value: item.id,
-        }));
-        setProductList(list);
-      }
-    });
+    service
+      .getProductList(
+        encodeQuery({
+          paging: false,
+          terms: {
+            state: 1,
+          },
+        }),
+      )
+      .then((resp: any) => {
+        if (resp.status === 200) {
+          const list = resp.result.map((item: { name: any; id: any }) => ({
+            label: item.name,
+            value: item.id,
+          }));
+          setProductList(list);
+        }
+      });
   }, []);
 
   const intlFormat = (

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

@@ -84,6 +84,7 @@ const Instance = () => {
           value: location.state[key],
         });
       });
+      console.log(_terms);
       setJumpParams([
         {
           terms: _terms,

+ 14 - 4
src/pages/device/Instance/service.ts

@@ -10,7 +10,10 @@ class Service extends BaseService<DeviceInstance> {
 
   // 查询产品列表
   public getProductList = (params?: any) =>
-    request(`/${SystemConst.API_BASE}/device/product/_query/no-paging`, { method: 'GET', params });
+    request(`/${SystemConst.API_BASE}/device/product/_query/no-paging?paging=false`, {
+      method: 'GET',
+      params,
+    });
 
   // 批量删除设备
   public batchDeleteDevice = (params: any) =>
@@ -33,6 +36,13 @@ class Service extends BaseService<DeviceInstance> {
       data: params,
     });
 
+  // 断开连接
+  public disconnectDevice = (deviceId: string, params?: any) =>
+    request(`/${SystemConst.API_BASE}/device-instance/${deviceId}/disconnect`, {
+      method: 'POST',
+      data: params,
+    });
+
   // 批量激活设备
   public batchDeployDevice = (params: any) =>
     request(`/${SystemConst.API_BASE}/device-instance/batch/_deploy`, {
@@ -206,14 +216,14 @@ class Service extends BaseService<DeviceInstance> {
     });
   // 读取属性
   public readProperties = (deviceId: string, data: any) =>
-    request(`/${SystemConst.API_BASE}/device-instance/${deviceId}/properties/_read`, {
+    request(`/${SystemConst.API_BASE}/device/instance/${deviceId}/properties/_read`, {
       method: 'POST',
       data,
     });
   // 设置属性
   public settingProperties = (deviceId: string, data: any) =>
-    request(`/${SystemConst.API_BASE}//device-instance/${deviceId}/property`, {
-      method: 'POST',
+    request(`/${SystemConst.API_BASE}/device/instance/${deviceId}/property`, {
+      method: 'PUT',
       data,
     });
   //获取协议设置的默认物模型

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

@@ -338,7 +338,6 @@ const Access = () => {
                   请先
                   <Button
                     type="link"
-                    disabled={!!(productModel.current?.count && productModel.current?.count > 0)}
                     onClick={() => {
                       setConfigVisible(true);
                     }}
@@ -362,20 +361,28 @@ const Access = () => {
                   data={
                     <span>
                       接入方式
-                      <Button
-                        size="small"
-                        type="primary"
-                        ghost
-                        style={{ marginLeft: 20 }}
-                        disabled={
+                      <Tooltip
+                        title={
                           !!(productModel.current?.count && productModel.current?.count > 0)
+                            ? '产品下有设备实例时不能更换接入方式'
+                            : ''
                         }
-                        onClick={() => {
-                          setConfigVisible(true);
-                        }}
                       >
-                        更换
-                      </Button>
+                        <Button
+                          size="small"
+                          type="primary"
+                          ghost
+                          style={{ marginLeft: 20 }}
+                          disabled={
+                            !!(productModel.current?.count && productModel.current?.count > 0)
+                          }
+                          onClick={() => {
+                            setConfigVisible(true);
+                          }}
+                        >
+                          更换
+                        </Button>
+                      </Tooltip>
                     </span>
                   }
                 />

+ 4 - 2
src/pages/link/AccessConfig/Detail/Provider/index.less

@@ -1,9 +1,11 @@
 .images {
+  display: flex;
+  align-items: center;
+  justify-content: center;
   width: 64px;
   height: 64px;
   color: white;
   font-size: 12px;
-  line-height: 64px;
   text-align: center;
   background: linear-gradient(
     128.453709216706deg,
@@ -27,7 +29,7 @@
 }
 
 .title {
-  width: '100%';
+  width: 100%;
   margin-bottom: 10px;
   overflow: hidden;
   font-weight: 800;

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

@@ -50,7 +50,7 @@ const Provider = (props: Props) => {
                     <div className={styles.images}>{item.name}</div>
                     <div style={{ margin: '10px', width: 'calc(100% - 84px)' }}>
                       <div style={{ fontWeight: 600 }}>{item.name}</div>
-                      <div className={styles.desc}>{item.description || '--'}</div>
+                      <div className={styles.desc}>{item?.description || '--'}</div>
                     </div>
                   </div>
                   <div style={{ width: '70px' }}>
@@ -92,7 +92,7 @@ const Provider = (props: Props) => {
                     <div className={styles.images}>{item.name}</div>
                     <div style={{ margin: '10px', width: 'calc(100% - 84px)' }}>
                       <div style={{ fontWeight: 600 }}>{item.name}</div>
-                      <div className={styles.desc}>{item.description}</div>
+                      <div className={styles.desc}>{item.description || '--'}</div>
                     </div>
                   </div>
                   <div style={{ width: '70px' }}>

+ 12 - 2
src/pages/link/Protocol/index.tsx

@@ -2,7 +2,7 @@ import { PageContainer } from '@ant-design/pro-layout';
 import type { ActionType, ProColumns } from '@jetlinks/pro-table';
 import type { ProtocolItem } from '@/pages/link/Protocol/typings';
 import { Badge, message } from 'antd';
-import { useRef, useState } from 'react';
+import { useEffect, useRef, useState } from 'react';
 import {
   DeleteOutlined,
   EditOutlined,
@@ -11,7 +11,7 @@ import {
   StopOutlined,
 } from '@ant-design/icons';
 import Service from '@/pages/link/Protocol/service';
-import { useIntl } from 'umi';
+import { useIntl, useLocation } from 'umi';
 import SearchComponent from '@/components/SearchComponent';
 import { PermissionButton, ProTableCard } from '@/components';
 import ProcotolCard from '@/components/ProTableCard/CardItems/protocol';
@@ -161,6 +161,15 @@ const Protocol = () => {
     },
   ];
 
+  const location = useLocation();
+
+  useEffect(() => {
+    if ((location as any).query?.save === 'true') {
+      setCurrent(undefined);
+      setVisible(true);
+    }
+  }, []);
+
   return (
     <PageContainer>
       <SearchComponent<ProtocolItem>
@@ -294,6 +303,7 @@ const Protocol = () => {
           }}
           reload={() => {
             actionRef.current?.reload();
+            setVisible(false);
           }}
         />
       )}

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

@@ -203,7 +203,7 @@ const Save = (props: Props) => {
   const save = async (deploy: boolean) => {
     const value = await form.submit<ProtocolItem>();
     let response = undefined;
-    if (props.data?.id) {
+    if (!props.data?.id) {
       response = await service.save(value);
     } else {
       response = await service.update(value);

+ 139 - 0
src/pages/rule-engine/Scene/Save/action/VariableItems/builtIn.tsx

@@ -0,0 +1,139 @@
+import { DatePicker, Input, InputNumber, Select } from 'antd';
+import { useCallback, useEffect, useState } from 'react';
+import { useRequest } from 'umi';
+import { queryBuiltInParams } from '@/pages/rule-engine/Scene/Save/action/service';
+import { ItemGroup } from '@/pages/rule-engine/Scene/Save/components';
+import moment from 'moment';
+
+type ChangeType = {
+  source?: string;
+  value?: string;
+  upperKey?: string;
+};
+
+interface BuiltInProps {
+  value?: ChangeType;
+  data?: any;
+  type?: string;
+  onChange?: (value: ChangeType) => void;
+}
+
+export default (props: BuiltInProps) => {
+  const [source, setSource] = useState(props.value?.source);
+  const [value, setValue] = useState(props.value?.value);
+  const [upperKey, setUpperKey] = useState(props.value?.upperKey);
+
+  const [builtInList, setBuiltInList] = useState([]);
+
+  const { run: getBuiltInList } = useRequest(queryBuiltInParams, {
+    manual: true,
+    formatResult: (res) => res.result,
+    onSuccess: (res) => {
+      setBuiltInList(res);
+    },
+  });
+
+  useEffect(() => {
+    if (source === 'upper') {
+      getBuiltInList({
+        trigger: { type: props.type },
+      });
+    }
+  }, [source, props.type]);
+
+  useEffect(() => {
+    setSource(props.value?.source);
+    setValue(props.value?.value);
+    setUpperKey(props.value?.upperKey);
+  }, [props.value]);
+
+  const onChange = (_source: string = 'fixed', _value?: any, _upperKey?: string) => {
+    const obj: ChangeType = {
+      source: _source,
+    };
+    if (_value) {
+      obj.value = _value;
+    }
+    if (_upperKey) {
+      obj.upperKey = _upperKey;
+    }
+
+    if (props.onChange) {
+      props.onChange(obj);
+    }
+  };
+
+  const itemOnChange = useCallback(
+    (_value: any) => {
+      onChange(source, _value);
+    },
+    [source],
+  );
+
+  const inputNodeByType = useCallback(
+    (data: any) => {
+      switch (data.type) {
+        case 'date':
+          return (
+            // @ts-ignore
+            <DatePicker
+              value={value ? moment(value) : undefined}
+              style={{ width: '100%' }}
+              format={data.format || 'YYYY-MM-DD HH:mm:ss'}
+              onChange={(date) => {
+                itemOnChange(date?.format(data.format || 'YYYY-MM-DD HH:mm:ss'));
+              }}
+            />
+          );
+        case 'number':
+          return (
+            <InputNumber
+              value={value}
+              placeholder={`请输入${data.name}`}
+              style={{ width: '100%' }}
+              onChange={itemOnChange}
+            />
+          );
+        default:
+          return (
+            <Input
+              value={value}
+              placeholder={`请输入${data.name}`}
+              onChange={(e) => itemOnChange(e.target.value)}
+            />
+          );
+      }
+    },
+    [value],
+  );
+
+  return (
+    <ItemGroup>
+      <Select
+        value={source}
+        options={[
+          { label: '手动输入', value: 'fixed' },
+          { label: '内置参数', value: 'upper' },
+        ]}
+        style={{ width: 120 }}
+        onChange={(key) => {
+          setSource(key);
+          onChange(key, undefined, undefined);
+        }}
+      ></Select>
+      {source === 'upper' ? (
+        <Select
+          value={upperKey}
+          options={builtInList}
+          onChange={(key) => {
+            onChange(source, undefined, key);
+          }}
+          fieldNames={{ label: 'name', value: 'id' }}
+          placeholder={'请选择参数'}
+        />
+      ) : (
+        <div>{inputNodeByType(props.data)}</div>
+      )}
+    </ItemGroup>
+  );
+};

+ 1 - 0
src/pages/rule-engine/Scene/Save/action/VariableItems/email.tsx

@@ -0,0 +1 @@
+export default () => {};

+ 4 - 0
src/pages/rule-engine/Scene/Save/action/VariableItems/index.ts

@@ -0,0 +1,4 @@
+export { default as UserList } from './user';
+export { default as OrgList } from './org';
+export { default as BuiltIn } from './builtIn';
+export { default as TagSelect } from './tag';

+ 63 - 0
src/pages/rule-engine/Scene/Save/action/VariableItems/org.tsx

@@ -0,0 +1,63 @@
+import { TreeSelect } from 'antd';
+import { useEffect, useState } from 'react';
+import {
+  queryDingTalkDepartments,
+  queryWechatDepartments,
+} from '@/pages/rule-engine/Scene/Save/action/service';
+
+type ChangeType = {
+  value?: string[];
+};
+
+interface OrgProps {
+  value?: ChangeType;
+  data?: any;
+  notifyType: string;
+  configId: string;
+  onChange?: (value: ChangeType) => void;
+}
+
+export default (props: OrgProps) => {
+  const [keys, setKeys] = useState<any[]>(props.value?.value || []);
+  const [departmentTree, setDepartmentTree] = useState([]);
+
+  const getDepartment = async (id: string) => {
+    if (props.notifyType === 'dingTalk') {
+      const resp = await queryDingTalkDepartments(id);
+      if (resp.status === 200) {
+        setDepartmentTree(resp.result);
+      }
+    } else {
+      const resp = await queryWechatDepartments(id);
+      if (resp.status === 200) {
+        setDepartmentTree(resp.result);
+      }
+    }
+  };
+
+  useEffect(() => {
+    getDepartment(props.configId);
+  }, []);
+
+  useEffect(() => {
+    setKeys(props.value?.value || []);
+  }, [props.value]);
+
+  return (
+    <TreeSelect
+      value={keys}
+      listHeight={200}
+      treeData={departmentTree}
+      fieldNames={{
+        label: 'name',
+        value: 'id',
+      }}
+      onChange={(key) => {
+        if (props.onChange) {
+          props.onChange({ value: key });
+        }
+      }}
+      placeholder={'请选择部门'}
+    />
+  );
+};

+ 50 - 0
src/pages/rule-engine/Scene/Save/action/VariableItems/tag.tsx

@@ -0,0 +1,50 @@
+import { Select } from 'antd';
+import { useEffect, useState } from 'react';
+import { queryTag } from '@/pages/rule-engine/Scene/Save/action/service';
+
+interface TagSelectProps {
+  configId?: string;
+  value?: string;
+  onChange?: (value: string) => void;
+}
+
+export default (props: TagSelectProps) => {
+  const [value, setValue] = useState<string | undefined>(props.value);
+  const [options, setOptions] = useState([]);
+
+  useEffect(() => {
+    if (props.configId) {
+      queryTag(props.configId).then((res) => {
+        if (res.status === 200) {
+          setOptions(res.result);
+        } else {
+          setOptions([]);
+        }
+      });
+    } else {
+      setOptions([]);
+    }
+  }, [props.configId]);
+
+  useEffect(() => {
+    setValue(props.value);
+  }, [props.value]);
+
+  return (
+    <Select
+      value={value}
+      placeholder={'请选择标签'}
+      options={options}
+      fieldNames={{
+        label: 'name',
+        value: 'id',
+      }}
+      style={{ width: '100%' }}
+      onChange={(key) => {
+        if (props.onChange) {
+          props.onChange(key);
+        }
+      }}
+    />
+  );
+};

+ 232 - 0
src/pages/rule-engine/Scene/Save/action/VariableItems/user.tsx

@@ -0,0 +1,232 @@
+// 收信人
+import { useEffect, useState } from 'react';
+import { ItemGroup } from '@/pages/rule-engine/Scene/Save/components';
+import { Select } from 'antd';
+import {
+  queryDingTalkUsers,
+  queryPlatformUsers,
+  queryRelationUsers,
+  queryWechatUsers,
+} from '@/pages/rule-engine/Scene/Save/action/service';
+
+type ChangeType = {
+  source?: string;
+  value?: string;
+  relation?: any;
+};
+
+interface UserProps {
+  notifyType: string;
+  configId: string;
+  value?: ChangeType;
+  type?: string;
+  onChange?: (value: ChangeType) => void;
+}
+
+export default (props: UserProps) => {
+  const [source, setSource] = useState(props.value?.source);
+  const [value, setValue] = useState<string | undefined>('');
+  const [userList, setUserList] = useState({ platform: [], relation: [] });
+  const [relationList, setRelationList] = useState([]);
+
+  useEffect(() => {
+    setSource(props.value?.source);
+    if (props.value?.source === 'relation') {
+      const relation = props.value?.relation;
+      console.log(relation);
+      if (relation) {
+        if (relation.objectId) {
+          // 平台用户
+          setValue(relation.objectId);
+        } else {
+          // 关系用户
+          setValue(relation.related?.relation);
+        }
+      }
+    } else {
+      setValue(props.value?.value);
+    }
+  }, [props.value]);
+
+  const getPlatformUser = async () => {
+    const _userList: any = {
+      platform: [],
+      relation: [],
+    };
+    const resp1 = await queryPlatformUsers();
+    if (resp1.status === 200) {
+      _userList.platform = resp1.result.map((item: any) => ({ label: item.name, value: item.id }));
+    }
+
+    const resp2 = await queryRelationUsers();
+    if (resp2.status === 200) {
+      _userList.relation = resp2.result.map((item: any) => ({
+        label: item.name,
+        value: item.relation,
+      }));
+    }
+
+    setUserList(_userList);
+  };
+
+  const getRelationUsers = async (notifyType: string, configId: string) => {
+    if (notifyType === 'dingTalk') {
+      const resp = await queryDingTalkUsers(configId);
+      if (resp.status === 200) {
+        setRelationList(resp.result);
+      }
+    } else {
+      const resp = await queryWechatUsers(configId);
+      if (resp.status === 200) {
+        setRelationList(resp.result);
+      }
+    }
+  };
+
+  useEffect(() => {
+    if (source === 'fixed') {
+      // 钉钉,微信用户
+      getRelationUsers(props.notifyType, props.configId);
+    } else {
+      getPlatformUser();
+    }
+  }, [source, props.notifyType]);
+
+  const options = [
+    { label: '平台用户', value: 'relation' },
+    { label: props.notifyType === 'dingTalk' ? '钉钉用户' : '微信用户', value: 'fixed' },
+  ];
+
+  /**
+   * 收信人-平台用户格式
+   * {
+   *   source: 'relation',
+   *   relation: {
+   *     related: {
+   *       objectType: 'user',
+   *       relation: 'userId'
+   *     }
+   *   }
+   * }
+   * 收信人-平台-关系用户格式
+   * {
+   *   source: 'relation',
+   *   relation: {
+   *     objectType: 'device',
+   *     objectSource: {
+   *       source: 'upper',
+   *       upperKey: 'deviceId'
+   *     },
+   *     related: {
+   *       objectType: 'user',
+   *       relation: 'userId'
+   *     }
+   *   }
+   * }
+   * 收信人-钉钉/微信用户
+   * {
+   *   source: 'relation',
+   *   value: 'userId'
+   * }
+   * @param _source
+   * @param _value
+   * @param type
+   */
+  const onchange = (_source: string = 'fixed', _value?: string, isRelation?: boolean) => {
+    const obj: any = {
+      source: _source,
+    };
+    if (_source === 'relation') {
+      if (isRelation) {
+        obj.relation = {
+          objectType: 'device',
+          objectSource: {
+            source: 'upper',
+            upperKey: 'deviceId',
+          },
+          related: {
+            objectType: 'user',
+            relation: _value,
+          },
+        };
+      } else {
+        obj.relation = {
+          objectType: 'user',
+          objectId: _value,
+        };
+      }
+    } else {
+      obj.value = _value;
+    }
+    console.log(obj);
+    if (props.onChange) {
+      props.onChange(obj);
+    }
+  };
+
+  const filterOption = (input: string, option: any) => {
+    return option.children ? option.children.toLowerCase().includes(input.toLowerCase()) : false;
+  };
+
+  return (
+    <ItemGroup>
+      <Select
+        value={source}
+        options={options}
+        style={{ width: 120 }}
+        onChange={(key) => {
+          setSource(key);
+          onchange(key, undefined);
+        }}
+      />
+      {source === 'relation' ? (
+        <Select
+          showSearch
+          value={value}
+          onChange={(key, node) => {
+            setValue(key);
+            onchange(source, key, node.isRelation);
+          }}
+          placeholder={'请选择收信人'}
+          listHeight={200}
+          filterOption={filterOption}
+        >
+          {userList.platform.length ? (
+            <Select.OptGroup label={'平台用户'}>
+              {userList.platform.map((item: any) => (
+                <Select.Option value={item.value} isRelation={false}>
+                  {item.label}
+                </Select.Option>
+              ))}
+            </Select.OptGroup>
+          ) : null}
+          {userList.relation.length ? (
+            <Select.OptGroup label={'关系用户'}>
+              {userList.relation.map((item: any) => (
+                <Select.Option value={item.value} isRelation={true}>
+                  {item.label}
+                </Select.Option>
+              ))}
+            </Select.OptGroup>
+          ) : null}
+        </Select>
+      ) : (
+        <Select
+          showSearch
+          value={value}
+          options={relationList}
+          listHeight={200}
+          onChange={(key) => {
+            setValue(key);
+            onchange(source, key);
+          }}
+          fieldNames={{ label: 'name', value: 'id' }}
+          placeholder={'请选择收信人'}
+          filterOption={(input: string, option: any) => {
+            return option.name ? option.name.toLowerCase().includes(input.toLowerCase()) : false;
+          }}
+        ></Select>
+      )}
+    </ItemGroup>
+  );
+};

+ 70 - 47
src/pages/rule-engine/Scene/Save/action/action.tsx

@@ -1,27 +1,40 @@
-import { Button, InputNumber, Select, Form } from 'antd';
 import type { FormInstance } from 'antd';
+import { Button, Form, Select } from 'antd';
 import { useEffect, useState } from 'react';
 import { useRequest } from 'umi';
 import {
-  queryMessageType,
   queryMessageConfig,
   queryMessageTemplate,
   queryMessageTemplateDetail,
+  queryMessageType,
 } from './service';
 import MessageContent from './messageContent';
+import DeviceSelect, { MessageTypeEnum } from './device';
+import WriteProperty from './device/WriteProperty';
+import ReadProperty from './device/readProperty';
+import FunctionCall from './device/functionCall';
+import { InputNumber } from '../components';
 
 interface ActionProps {
   restField: any;
   name: number;
   form: FormInstance;
   title?: string;
+  triggerType: string;
   onRemove: () => void;
 }
 
 const ActionItem = (props: ActionProps) => {
   const { name } = props;
   const [type1, setType1] = useState('');
+  // 消息通知
+  const [notifyType, setNotifyType] = useState('');
+  const [configId, setConfigId] = useState('');
   const [templateData, setTemplateData] = useState<any>(undefined);
+  // 设备输出
+  const [deviceMessageType, setDeviceMessageType] = useState('WRITE_PROPERTY');
+  const [properties, setProperties] = useState([]); // 物模型-属性
+  const [functionList, setFunctionList] = useState([]); // 物模型-功能
 
   const { data: messageType, run: queryMessageTypes } = useRequest(queryMessageType, {
     manual: true,
@@ -56,6 +69,7 @@ const ActionItem = (props: ActionProps) => {
           style={{ width: 140 }}
           onChange={async (key: string) => {
             setTemplateData(undefined);
+            setNotifyType(key);
             props.form.resetFields([['actions', name, 'notify', 'notifierId']]);
             props.form.resetFields([['actions', name, 'notify', 'templateId']]);
             await queryMessageConfigs({
@@ -69,11 +83,16 @@ const ActionItem = (props: ActionProps) => {
           options={messageConfig}
           loading={messageConfigLoading}
           fieldNames={{ value: 'id', label: 'name' }}
-          onChange={async (key: string) => {
+          onChange={async (key: string, node: any) => {
+            setConfigId(key);
             setTemplateData(undefined);
             props.form.resetFields([['actions', name, 'notify', 'templateId']]);
+
             await queryMessageTemplates({
-              terms: [{ column: 'configId', value: key }],
+              terms: [
+                { column: 'type', value: notifyType },
+                { column: 'provider', value: node.provider },
+              ],
             });
           }}
           style={{ width: 160 }}
@@ -98,48 +117,12 @@ const ActionItem = (props: ActionProps) => {
     </>
   );
 
-  const DeviceNodes = (
-    <>
-      <Select options={[]} placeholder={'请选择产品'} style={{ width: 220 }} />
-      <Select
-        defaultValue={'1'}
-        options={[
-          { label: '固定设备', value: '1' },
-          { label: '按标签', value: '2' },
-          { label: '按关系', value: '3' },
-        ]}
-        style={{ width: 120 }}
-      />
-      <Select options={[]} placeholder={'请选择'} style={{ width: 180 }} />
-      <Select
-        defaultValue={'1'}
-        options={[
-          { label: '设置属性', value: '1' },
-          { label: '功能调用', value: '2' },
-          { label: '读取属性', value: '3' },
-        ]}
-        style={{ width: 120 }}
-      />
-    </>
-  );
-
   useEffect(() => {
-    if (type1 === 'message') {
+    if (type1 === 'notify') {
       queryMessageTypes();
     }
   }, [type1]);
 
-  const TimeTypeAfter = (
-    <Select
-      defaultValue={'second'}
-      options={[
-        { label: '秒', value: 'second' },
-        { label: '分', value: 'minute' },
-        { label: '小时', value: 'hour' },
-      ]}
-    />
-  );
-
   return (
     <div className={'actions-item'}>
       <div className={'actions-item-title'}>
@@ -152,7 +135,7 @@ const ActionItem = (props: ActionProps) => {
         <Form.Item {...props.restField} name={[name, 'executor']}>
           <Select
             options={[
-              { label: '消息通知', value: 'message' },
+              { label: '消息通知', value: 'notify' },
               { label: '设备输出', value: 'device' },
               { label: '延迟执行', value: 'delay' },
             ]}
@@ -163,14 +146,54 @@ const ActionItem = (props: ActionProps) => {
             }}
           />
         </Form.Item>
-        {type1 === 'message' && MessageNodes}
-        {type1 === 'device' && DeviceNodes}
+        {type1 === 'notify' && MessageNodes}
+        {type1 === 'device' && (
+          <DeviceSelect
+            name={props.name}
+            form={props.form}
+            triggerType={props.triggerType}
+            onProperties={setProperties}
+            onMessageTypeChange={setDeviceMessageType}
+            onFunctionChange={setFunctionList}
+            restField={props.restField}
+          />
+        )}
         {type1 === 'delay' && (
-          <InputNumber addonAfter={TimeTypeAfter} style={{ width: 150 }} min={0} max={9999} />
+          <Form.Item name={[name, 'delay']}>
+            <InputNumber />
+          </Form.Item>
         )}
       </div>
-      {type1 === 'message' && templateData ? (
-        <MessageContent form={props.form} template={templateData} name={props.name} />
+      {type1 === 'notify' && templateData ? (
+        <MessageContent
+          form={props.form}
+          template={templateData}
+          name={props.name}
+          notifyType={notifyType}
+          triggerType={props.triggerType}
+          configId={configId}
+        />
+      ) : null}
+      {type1 === 'device' &&
+      deviceMessageType === MessageTypeEnum.WRITE_PROPERTY &&
+      properties.length ? (
+        <Form.Item name={[name, 'device', 'message', 'properties']}>
+          <WriteProperty properties={properties} type={props.triggerType} form={props.form} />
+        </Form.Item>
+      ) : null}
+      {type1 === 'device' &&
+      deviceMessageType === MessageTypeEnum.READ_PROPERTY &&
+      properties.length ? (
+        <Form.Item name={[name, 'device', 'message', 'properties']}>
+          <ReadProperty properties={properties} />
+        </Form.Item>
+      ) : null}
+      {type1 === 'device' &&
+      deviceMessageType === MessageTypeEnum.INVOKE_FUNCTION &&
+      functionList.length ? (
+        <Form.Item name={[name, 'device', 'message', 'inputs']}>
+          <FunctionCall functionData={functionList} />
+        </Form.Item>
       ) : null}
     </div>
   );

+ 152 - 0
src/pages/rule-engine/Scene/Save/action/device/WriteProperty/index.tsx

@@ -0,0 +1,152 @@
+import { DatePicker, FormInstance, Input, InputNumber, Select, Space } from 'antd';
+import { useCallback, useEffect, useState } from 'react';
+import { useRequest } from '@@/plugin-request/request';
+import { queryBuiltInParams } from '@/pages/rule-engine/Scene/Save/action/service';
+import moment from 'moment';
+
+interface WritePropertyProps {
+  properties: any[];
+  type: string;
+  form: FormInstance;
+  value?: any;
+  onChange?: (value?: any) => void;
+}
+
+export default (props: WritePropertyProps) => {
+  console.log(props.properties);
+  const [source, setSource] = useState('fixed');
+  const [builtInList, setBuiltInList] = useState([]);
+  const [propertiesKey, setPropertiesKey] = useState<string | undefined>(undefined);
+  const [propertiesValue, setPropertiesValue] = useState(undefined);
+  const [propertiesType, setPropertiesType] = useState('');
+
+  const { run: getBuiltInList } = useRequest(queryBuiltInParams, {
+    manual: true,
+    formatResult: (res) => res.result,
+    onSuccess: (res) => {
+      setBuiltInList(res);
+    },
+  });
+
+  useEffect(() => {
+    if (source === 'upper') {
+      getBuiltInList({
+        trigger: { type: props.type },
+      });
+    }
+  }, [source, props.type]);
+
+  useEffect(() => {
+    if (props.value) {
+      if (0 in props.value) {
+        setPropertiesValue(props.value[0]);
+      } else {
+        Object.keys(props.value).forEach((key: string) => {
+          setPropertiesKey(key);
+          setPropertiesValue(props.value[key]);
+        });
+      }
+    }
+  }, [props.value]);
+
+  const onChange = (key?: string, value?: any) => {
+    if (props.onChange) {
+      props.onChange({
+        [key || 0]: value,
+      });
+    }
+  };
+
+  const inputNodeByType = useCallback(
+    (type: string) => {
+      switch (type) {
+        case 'boolean':
+          return (
+            <Select
+              style={{ width: 300, textAlign: 'left' }}
+              value={propertiesValue}
+              options={[
+                { label: 'true', value: true },
+                { label: 'false', value: false },
+              ]}
+              placeholder={'请选择'}
+              onChange={(value) => {
+                onChange(propertiesKey, value);
+              }}
+            />
+          );
+        case 'int':
+        case 'long':
+        case 'float':
+        case 'double':
+          return (
+            <InputNumber
+              style={{ width: 300 }}
+              value={propertiesValue}
+              placeholder={'请输入'}
+              onChange={(value) => {
+                onChange(propertiesKey, value);
+              }}
+            />
+          );
+        case 'date':
+          return (
+            // @ts-ignore
+            <DatePicker
+              style={{ width: 300 }}
+              value={propertiesValue ? moment(propertiesValue) : undefined}
+              onChange={(date) => {
+                onChange(propertiesKey, date ? date.format('YYYY-MM-DD HH:mm:ss') : undefined);
+              }}
+            />
+          );
+        default:
+          return (
+            <Input
+              style={{ width: 300 }}
+              value={propertiesValue}
+              placeholder={'请输入'}
+              onChange={(e) => onChange(propertiesKey, e.target.value)}
+            />
+          );
+      }
+    },
+    [propertiesKey, propertiesValue],
+  );
+
+  return (
+    <Space>
+      <Select
+        value={propertiesKey}
+        options={props.properties}
+        fieldNames={{ label: 'name', value: 'id' }}
+        style={{ width: 120 }}
+        onSelect={(key: any, node: any) => {
+          onChange(key, undefined);
+          setPropertiesType(node.valueType.type);
+        }}
+        placeholder={'请选择属性'}
+      ></Select>
+      <Select
+        value={source}
+        options={[
+          { label: '手动输入', value: 'fixed' },
+          { label: '内置参数', value: 'upper' },
+        ]}
+        style={{ width: 120 }}
+        onChange={(key) => {
+          setSource(key);
+        }}
+      />
+      {source === 'upper' ? (
+        <Select
+          options={builtInList}
+          fieldNames={{ label: 'name', value: 'id' }}
+          placeholder={'请选择参数'}
+        />
+      ) : (
+        <div>{inputNodeByType(propertiesType)}</div>
+      )}
+    </Space>
+  );
+};

+ 179 - 0
src/pages/rule-engine/Scene/Save/action/device/deviceModal.tsx

@@ -0,0 +1,179 @@
+import { Badge, Input, message, Modal } from 'antd';
+import { useEffect, useRef, useState } from 'react';
+import ProTable, { ActionType, ProColumns } from '@jetlinks/pro-table';
+import { DeviceItem } from '@/pages/system/Department/typings';
+import { useIntl } from '@@/plugin-locale/localeExports';
+import SearchComponent from '@/components/SearchComponent';
+import { queryDevice } from '@/pages/rule-engine/Scene/Save/action/device/service';
+
+interface DeviceModelProps {
+  value?: ChangeValueType[];
+  onChange?: (value: ChangeValueType[]) => void;
+  productId?: string;
+}
+
+type DeviceBadgeProps = {
+  type: string;
+  text: string;
+};
+
+type ChangeValueType = {
+  name: string;
+  value: string;
+};
+
+const DeviceBadge = (props: DeviceBadgeProps) => {
+  const STATUS = {
+    notActive: 'processing',
+    offline: 'error',
+    online: 'success',
+  };
+  return <Badge status={STATUS[props.type]} text={props.text} />;
+};
+
+export default (props: DeviceModelProps) => {
+  const intl = useIntl();
+  const actionRef = useRef<ActionType>();
+  const [visible, setVisible] = useState(false);
+  const [selectKeys, setSelectKeys] = useState<ChangeValueType[]>(props.value || []);
+  const [searchParam, setSearchParam] = useState({});
+  const [value, setValue] = useState<ChangeValueType[]>(props.value || []);
+
+  useEffect(() => {
+    setValue(props.value || []);
+    setSelectKeys(props.value || []);
+  }, [props.value]);
+
+  const columns: ProColumns<DeviceItem>[] = [
+    {
+      dataIndex: 'id',
+      title: 'ID',
+      width: 220,
+    },
+    {
+      dataIndex: 'name',
+      title: intl.formatMessage({
+        id: 'pages.table.name',
+        defaultMessage: '名称',
+      }),
+    },
+    {
+      title: intl.formatMessage({
+        id: 'pages.device.instance.registrationTime',
+        defaultMessage: '注册时间',
+      }),
+      dataIndex: 'registryTime',
+      valueType: 'dateTime',
+    },
+    {
+      title: intl.formatMessage({
+        id: 'pages.searchTable.titleStatus',
+        defaultMessage: '状态',
+      }),
+      dataIndex: 'state',
+      valueType: 'select',
+      valueEnum: {
+        all: {
+          text: intl.formatMessage({
+            id: 'pages.searchTable.titleStatus.all',
+            defaultMessage: '全部',
+          }),
+          status: 'Default',
+        },
+        onLine: {
+          text: intl.formatMessage({
+            id: 'pages.device.instance.status.onLine',
+            defaultMessage: '在线',
+          }),
+          status: 'onLine',
+        },
+        offLine: {
+          text: intl.formatMessage({
+            id: 'pages.device.instance.status.offLine',
+            defaultMessage: '离线',
+          }),
+          status: 'offLine',
+        },
+        notActive: {
+          text: intl.formatMessage({
+            id: 'pages.device.instance.status.notActive',
+            defaultMessage: '未启用',
+          }),
+          status: 'notActive',
+        },
+      },
+      search: false,
+      render: (_, row) => <DeviceBadge type={row.state.value} text={row.state.text} />,
+    },
+  ];
+
+  return (
+    <>
+      {visible && (
+        <Modal
+          visible={visible}
+          title={'设备'}
+          width={880}
+          onOk={() => {
+            if (props.onChange) {
+              props.onChange(selectKeys);
+            }
+            setVisible(false);
+            actionRef.current?.reload();
+          }}
+          onCancel={() => {
+            setVisible(false);
+            actionRef.current?.reload();
+            setSelectKeys([]);
+          }}
+        >
+          <SearchComponent<DeviceItem>
+            field={columns}
+            enableSave={false}
+            // pattern={'simple'}
+            onSearch={async (data) => {
+              actionRef.current?.reset?.();
+              setSearchParam(data);
+            }}
+            defaultParam={[{ column: 'productId', value: props.productId! }]}
+            target="scene-actions-device"
+          />
+          <ProTable<DeviceItem>
+            actionRef={actionRef}
+            columns={columns}
+            rowKey="id"
+            search={false}
+            rowSelection={{
+              selectedRowKeys: selectKeys.map((item) => item.value),
+              onSelect: (selectedRow: any, selected: any) => {
+                let newSelectKeys = [...selectKeys];
+                if (selected) {
+                  newSelectKeys.push({ name: selectedRow.name, value: selectedRow.id });
+                } else {
+                  newSelectKeys = newSelectKeys.filter((item) => item.value !== selectedRow.id);
+                }
+                setSelectKeys(newSelectKeys);
+              },
+            }}
+            request={(params) => queryDevice(params)}
+            params={searchParam}
+          ></ProTable>
+        </Modal>
+      )}
+      <Input
+        placeholder={'请选择设备'}
+        onClick={() => {
+          if (!props.productId) {
+            message.warning('请选择产品');
+          } else {
+            setVisible(true);
+            setSelectKeys([...value]);
+          }
+        }}
+        style={{ width: 300 }}
+        value={value.map((item) => item.name).toString()}
+        readOnly
+      />
+    </>
+  );
+};

+ 151 - 0
src/pages/rule-engine/Scene/Save/action/device/functionCall.tsx

@@ -0,0 +1,151 @@
+import type { ProColumns } from '@jetlinks/pro-table';
+import { EditableProTable } from '@jetlinks/pro-table';
+import { DatePicker, Input, InputNumber, Select } from 'antd';
+import React, { useEffect, useRef, useState } from 'react';
+import type { ProFormInstance } from '@ant-design/pro-form';
+import ProForm from '@ant-design/pro-form';
+import moment from 'moment';
+
+type FunctionTableDataType = {
+  id: string;
+  name: string;
+  type: string;
+};
+
+interface FunctionCallProps {
+  functionData: any[];
+  value?: any;
+  onChange?: (data: any) => void;
+}
+
+export default (props: FunctionCallProps) => {
+  const [editableKeys, setEditableRowKeys] = useState<React.Key[]>([]);
+  const formRef = useRef<ProFormInstance<any>>();
+
+  useEffect(() => {
+    setEditableRowKeys(props.functionData.map((d) => d.id));
+    formRef.current?.setFieldsValue({
+      table: props.functionData,
+    });
+  }, [props.functionData]);
+
+  useEffect(() => {
+    if (props.functionData && props.functionData.length && props.value) {
+      formRef.current?.setFieldsValue({
+        table: props.functionData.map((item: any) => {
+          const oldValue = props.value.find((oldItem: any) => oldItem.name === item.id);
+          if (oldValue) {
+            return {
+              ...item,
+              value: oldValue.value,
+            };
+          }
+          return item;
+        }),
+      });
+    }
+    console.log(props.value);
+  }, []);
+
+  const getItemNode = (record: any) => {
+    const type = record.type;
+    const name = record.name;
+
+    switch (type) {
+      case 'enum':
+        return (
+          <Select
+            style={{ width: '100%', textAlign: 'left' }}
+            options={record.options}
+            fieldNames={{ label: 'text', value: 'value' }}
+            placeholder={'请选择' + name}
+          />
+        );
+      case 'boolean':
+        return (
+          <Select
+            style={{ width: '100%', textAlign: 'left' }}
+            options={[
+              { label: 'true', value: true },
+              { label: 'false', value: false },
+            ]}
+            placeholder={'请选择' + name}
+          />
+        );
+      case 'int':
+      case 'long':
+      case 'float':
+      case 'double':
+        return <InputNumber style={{ width: '100%' }} placeholder={'请输入' + name} />;
+      case 'date':
+        return (
+          <>
+            {
+              // @ts-ignore
+              <DatePicker
+                format={record.format || 'YYYY-MM-DD HH:mm:ss'}
+                style={{ width: '100%' }}
+              />
+            }
+          </>
+        );
+      default:
+        return <Input placeholder={'请输入' + name} />;
+    }
+  };
+
+  const columns: ProColumns<FunctionTableDataType>[] = [
+    {
+      dataIndex: 'name',
+      title: '参数名称',
+      width: 200,
+      editable: false,
+    },
+    {
+      dataIndex: 'type',
+      title: '类型',
+      width: 200,
+      editable: false,
+    },
+    {
+      title: '值',
+      dataIndex: 'value',
+      align: 'center',
+      width: 260,
+      renderFormItem: (_, row: any) => {
+        return getItemNode(row.record);
+      },
+    },
+  ];
+
+  return (
+    <ProForm<{ table: FunctionTableDataType[] }>
+      formRef={formRef}
+      submitter={false}
+      onValuesChange={() => {
+        const values = formRef.current?.getFieldsValue();
+        if (props.onChange) {
+          props.onChange(
+            values.table.map((item: any) => ({
+              name: item.id,
+              value: item.type === 'date' ? moment(item.value).format(item.format) : item.value,
+            })),
+          );
+        }
+      }}
+    >
+      <EditableProTable
+        rowKey="id"
+        name="table"
+        columns={columns}
+        recordCreatorProps={false}
+        size={'small'}
+        editable={{
+          type: 'multiple',
+          editableKeys,
+          onChange: setEditableRowKeys,
+        }}
+      />
+    </ProForm>
+  );
+};

+ 182 - 0
src/pages/rule-engine/Scene/Save/action/device/index.tsx

@@ -0,0 +1,182 @@
+import type { FormInstance } from 'antd';
+import { Form, Input, Select } from 'antd';
+import { useEffect, useState } from 'react';
+import { getProductList } from '@/pages/rule-engine/Scene/Save/action/device/service';
+import Device from './deviceModal';
+import TagModal from './tagModal';
+
+interface DeviceProps {
+  name: number;
+  triggerType: string;
+  form?: FormInstance;
+  restField?: any;
+  onProperties: (data: any) => void;
+  onMessageTypeChange: (type: string) => void;
+  onFunctionChange: (functionItem: any) => void;
+}
+
+enum SourceEnum {
+  'fixed' = 'fixed',
+  'tag' = 'tag',
+  'relation' = '',
+}
+
+const DefaultSourceOptions = [
+  { label: '固定设备', value: SourceEnum.fixed },
+  { label: '按标签', value: SourceEnum.tag },
+];
+
+export enum MessageTypeEnum {
+  'WRITE_PROPERTY' = 'WRITE_PROPERTY',
+  'READ_PROPERTY' = 'READ_PROPERTY',
+  'INVOKE_FUNCTION' = 'INVOKE_FUNCTION',
+}
+
+export default (props: DeviceProps) => {
+  const { name } = props;
+
+  const [productId, setProductId] = useState<string>('');
+  const [sourceList, setSourceList] = useState(DefaultSourceOptions);
+  const [productList, setProductList] = useState<any[]>([]);
+  const [selector, setSelector] = useState(SourceEnum.fixed);
+  const [messageType, setMessageType] = useState(MessageTypeEnum.WRITE_PROPERTY);
+  const [functionList, setFunctionList] = useState([]);
+  const [tagList, setTagList] = useState([]);
+
+  const getProducts = async () => {
+    const resp = await getProductList({ paging: false });
+    if (resp && resp.status === 200) {
+      setProductList(resp.result);
+    }
+  };
+  useEffect(() => {
+    props.form?.resetFields([['actions', name, 'device', 'selector']]);
+    if (props.triggerType === 'device') {
+      setSourceList([...DefaultSourceOptions, { label: '按关系', value: SourceEnum.relation }]);
+    } else {
+      setSourceList(DefaultSourceOptions);
+    }
+  }, [props.triggerType]);
+
+  useEffect(() => {
+    getProducts();
+  }, []);
+
+  const handleMetadata = (metadata?: string) => {
+    try {
+      const metadataObj = JSON.parse(metadata || '{}');
+      if (props.onProperties) {
+        props.onProperties(metadataObj.properties || []);
+      }
+      if (!metadataObj.functions) {
+        if (props.onFunctionChange) {
+          props.onFunctionChange([]);
+        }
+      }
+      setTagList(metadataObj.tags || []);
+      setFunctionList(metadataObj.functions || []);
+    } catch (err) {
+      console.warn('handleMetadata === ', err);
+    }
+  };
+
+  return (
+    <>
+      <Form.Item name={[name, 'device', 'productId']}>
+        <Select
+          options={productList}
+          placeholder={'请选择产品'}
+          style={{ width: 220 }}
+          listHeight={220}
+          onChange={(key: any, node: any) => {
+            props.form?.resetFields([['actions', name, 'device', 'selector']]);
+            props.form?.resetFields([['actions', name, 'device', 'selectorValues']]);
+            props.form?.resetFields([['actions', name, 'device', 'message', 'functionId']]);
+            // setMessageType(MessageTypeEnum.WRITE_PROPERTY)
+            setProductId(key);
+            handleMetadata(node.metadata);
+          }}
+          fieldNames={{ label: 'name', value: 'id' }}
+        />
+      </Form.Item>
+      <Form.Item
+        name={[name, 'device', 'selector']}
+        initialValue={SourceEnum.fixed}
+        {...props.restField}
+      >
+        <Select
+          options={sourceList}
+          style={{ width: 120 }}
+          onChange={(key) => {
+            setSelector(key);
+          }}
+        />
+      </Form.Item>
+      {selector === SourceEnum.fixed && (
+        <Form.Item name={[name, 'device', 'selectorValues']} {...props.restField}>
+          <Device productId={productId} />
+        </Form.Item>
+      )}
+      {selector === SourceEnum.tag && (
+        <Form.Item name={[name, 'device', 'selectorValues']} {...props.restField}>
+          <TagModal tagData={tagList} />
+        </Form.Item>
+      )}
+      {selector === SourceEnum.relation && (
+        <Form.Item name={[name, 'device', 'selectorValues']} {...props.restField}>
+          <Select style={{ width: 300 }} />
+        </Form.Item>
+      )}
+      <Form.Item
+        name={[name, 'device', 'message', 'messageType']}
+        initialValue={MessageTypeEnum.WRITE_PROPERTY}
+        {...props.restField}
+      >
+        <Select
+          options={[
+            { label: '功能调用', value: MessageTypeEnum.INVOKE_FUNCTION },
+            { label: '读取属性', value: MessageTypeEnum.READ_PROPERTY },
+            { label: '设置属性', value: MessageTypeEnum.WRITE_PROPERTY },
+          ]}
+          onSelect={(key: any) => {
+            if (props.onMessageTypeChange) {
+              props.onMessageTypeChange(key);
+            }
+            setMessageType(key);
+          }}
+          style={{ width: 120 }}
+        />
+      </Form.Item>
+      {messageType === MessageTypeEnum.INVOKE_FUNCTION ? (
+        <Form.Item name={[name, 'device', 'message', 'functionId']}>
+          <Select
+            options={functionList}
+            fieldNames={{ label: 'name', value: 'id' }}
+            style={{ width: 120 }}
+            placeholder={'请选择功能'}
+            onSelect={(_: any, data: any) => {
+              const properties = data.valueType ? data.valueType.properties : data.inputs;
+              if (props.onFunctionChange) {
+                const array = [];
+                for (const datum of properties) {
+                  array.push({
+                    id: datum.id,
+                    name: datum.name,
+                    type: datum.valueType ? datum.valueType.type : '-',
+                    format: datum.valueType ? datum.valueType.format : undefined,
+                    options: datum.valueType ? datum.valueType.elements : undefined,
+                    value: undefined,
+                  });
+                }
+                props.onFunctionChange(array);
+              }
+            }}
+          />
+        </Form.Item>
+      ) : null}
+      <Form.Item name={[name, 'device', 'source']} hidden>
+        <Input />
+      </Form.Item>
+    </>
+  );
+};

+ 24 - 0
src/pages/rule-engine/Scene/Save/action/device/readProperty.tsx

@@ -0,0 +1,24 @@
+import { Select } from 'antd';
+
+interface ReadPropertyProps {
+  properties: any[];
+  value?: any;
+  onChange?: (value?: any) => void;
+}
+
+export default (props: ReadPropertyProps) => {
+  return (
+    <Select
+      value={props.value ? props.value[0] : undefined}
+      options={props.properties}
+      fieldNames={{ label: 'name', value: 'id' }}
+      style={{ width: 120 }}
+      onSelect={(key: any) => {
+        if (props.onChange) {
+          props.onChange([key]);
+        }
+      }}
+      placeholder={'请选择属性'}
+    ></Select>
+  );
+};

+ 13 - 0
src/pages/rule-engine/Scene/Save/action/device/service.ts

@@ -0,0 +1,13 @@
+import { request } from '@@/plugin-request/request';
+import SystemConst from '@/utils/const';
+
+// 获取设备
+export const queryDevice = (data: any) =>
+  request(`${SystemConst.API_BASE}/device-instance/_query`, {
+    method: 'POST',
+    data: data,
+  });
+
+// 获取产品
+export const getProductList = (params?: any) =>
+  request(`/${SystemConst.API_BASE}/device/product/_query/no-paging`, { method: 'GET', params });

+ 0 - 0
src/pages/rule-engine/Scene/Save/action/device/sourceItem.tsx


+ 263 - 0
src/pages/rule-engine/Scene/Save/action/device/tagModal.tsx

@@ -0,0 +1,263 @@
+import { Button, Col, DatePicker, Input, InputNumber, Modal, Row, Select, Space } from 'antd';
+import { useEffect, useState } from 'react';
+import moment from 'moment';
+import { DeleteOutlined, PlusOutlined } from '@ant-design/icons';
+
+type TagValueType = {
+  column: string;
+  value: any;
+  type: string;
+};
+
+interface TagModalProps {
+  tagData: any[];
+  value?: TagValueType[];
+  onChange?: (value: any[]) => void;
+}
+
+/**
+ * 数据格式 [{"value":{"key":"value"},"name":"标签名称"}]
+ * @param props
+ */
+export default (props: TagModalProps) => {
+  const [visible, setVisible] = useState(false);
+  const [tagList, setTagList] = useState<any[]>([{}]);
+  const [options, setOptions] = useState<any[]>([]);
+  const [nameList, setNameList] = useState<string[]>([]);
+
+  const handleItem = (data: any) => {
+    return {
+      ...data,
+      valueType: data.valueType ? data.valueType.type : '-',
+      format: data.valueType ? data.valueType.format : undefined,
+      options: data.valueType ? data.valueType.elements : undefined,
+      value: data.value,
+    };
+  };
+
+  useEffect(() => {
+    if (visible) {
+      setOptions(
+        props.tagData.map((item: any) => {
+          return { label: item.name, value: item.id, ...item };
+        }),
+      );
+    }
+  }, [visible, props.tagData]);
+
+  useEffect(() => {
+    if (props.value) {
+      const names: string[] = [];
+      const newTagList = props.value
+        .filter((valueItem) => {
+          return props.tagData.some((item) => valueItem.column === item.id);
+        })
+        .map((valueItem) => {
+          const oldItem = props.tagData.find((item) => item.id === valueItem.column);
+          if (oldItem) {
+            names.push(oldItem.name);
+            return {
+              ...handleItem(oldItem),
+              type: valueItem.type,
+            };
+          }
+          return valueItem;
+        });
+      setNameList(names);
+      setTagList(newTagList);
+    } else {
+      setTagList([{}]);
+    }
+  }, [props.value]);
+
+  const getItemNode = (record: any) => {
+    const type = record.valueType;
+    const name = record.name;
+
+    switch (type) {
+      case 'enum':
+        return (
+          <Select
+            value={record.value}
+            style={{ width: '100%', textAlign: 'left' }}
+            options={record.options}
+            fieldNames={{ label: 'text', value: 'value' }}
+            placeholder={'请选择' + name}
+            onChange={(key) => {
+              record.value = key;
+            }}
+          />
+        );
+      case 'boolean':
+        return (
+          <Select
+            value={record.value}
+            style={{ width: '100%', textAlign: 'left' }}
+            options={[
+              { label: 'true', value: true },
+              { label: 'false', value: false },
+            ]}
+            placeholder={'请选择' + name}
+            onChange={(key) => {
+              record.value = key;
+            }}
+          />
+        );
+      case 'int':
+      case 'long':
+      case 'float':
+      case 'double':
+        return (
+          <InputNumber
+            value={record.value}
+            style={{ width: '100%' }}
+            placeholder={'请输入' + name}
+            onChange={(key) => {
+              record.value = key;
+            }}
+          />
+        );
+      case 'date':
+        return (
+          <>
+            {
+              // @ts-ignore
+              <DatePicker
+                value={record.value && moment(record.value)}
+                format={record.format || 'YYYY-MM-DD HH:mm:ss'}
+                style={{ width: '100%' }}
+                onChange={(_, date) => {
+                  record.value = date;
+                }}
+              />
+            }
+          </>
+        );
+      default:
+        return (
+          <Input
+            value={record.value}
+            placeholder={'请输入标签值'}
+            onChange={(e) => {
+              record.value = e.target.value;
+            }}
+          />
+        );
+    }
+  };
+
+  return (
+    <>
+      <Modal
+        visible={visible}
+        title={'设备'}
+        width={660}
+        onOk={() => {
+          const newValue = tagList
+            .filter((item) => !!item.value)
+            .map((item: any) => {
+              return {
+                column: item.id,
+                type: item.type,
+                value: item.value,
+              };
+            });
+          if (props.onChange) {
+            props.onChange(newValue);
+          }
+          setVisible(false);
+          setTagList([{}]);
+        }}
+        onCancel={() => {
+          setVisible(false);
+          setTagList([{}]);
+          setOptions([]);
+        }}
+      >
+        <div>
+          {tagList.map((tag, index) => (
+            <Row gutter={12} key={tag.id || index} style={{ marginBottom: 12 }}>
+              <Col span={4}>
+                {index === 0 ? (
+                  <span>标签选择</span>
+                ) : (
+                  <Select
+                    value={tag.type}
+                    options={[
+                      { label: '并且', value: 'and' },
+                      { label: '或者', value: 'or' },
+                    ]}
+                    style={{ width: '100%' }}
+                    onSelect={(key: string) => {
+                      const indexItem = tagList[index];
+                      indexItem.type = key;
+                      tagList[index] = indexItem;
+                      setTagList([...tagList]);
+                    }}
+                  />
+                )}
+              </Col>
+              <Col span={16}>
+                <Row gutter={12}>
+                  <Col flex="120px">
+                    <Select
+                      value={tag.id}
+                      style={{ width: '120px' }}
+                      options={options}
+                      onSelect={(_: any, data: any) => {
+                        const newList = [...tagList];
+                        const indexType = newList[index].type;
+                        newList.splice(
+                          index,
+                          1,
+                          handleItem({ ...data, value: undefined, type: indexType }),
+                        );
+                        setTagList(newList);
+                      }}
+                      placeholder={'请选择标签'}
+                    />
+                  </Col>
+                  <Col flex={'auto'}>{getItemNode(tag)}</Col>
+                </Row>
+              </Col>
+              <Col span={4}>
+                <Space>
+                  <Button
+                    style={{ padding: '0 8px' }}
+                    onClick={() => {
+                      setTagList([...tagList, { type: 'and' }]);
+                    }}
+                  >
+                    <PlusOutlined />
+                  </Button>
+                  {tagList.length !== 1 && (
+                    <Button
+                      style={{ padding: '0 8px' }}
+                      onClick={() => {
+                        const newTagList = [...tagList];
+                        newTagList.splice(index, 1);
+                        setTagList(newTagList);
+                      }}
+                      danger
+                    >
+                      <DeleteOutlined />
+                    </Button>
+                  )}
+                </Space>
+              </Col>
+            </Row>
+          ))}
+        </div>
+      </Modal>
+      <Input
+        value={nameList.length ? nameList.toString() : undefined}
+        readOnly
+        style={{ width: 300 }}
+        onClick={() => {
+          setVisible(true);
+        }}
+        placeholder={'请选择标签'}
+      />
+    </>
+  );
+};

+ 41 - 133
src/pages/rule-engine/Scene/Save/action/messageContent.tsx

@@ -1,66 +1,25 @@
-import { Col, Form, Row, Select, Input, DatePicker, InputNumber } from 'antd';
 import type { FormInstance } from 'antd';
-import { ItemGroup, InputFile } from '@/pages/rule-engine/Scene/Save/components';
-import { useEffect, useState } from 'react';
-import { TriggerWayType } from '@/pages/rule-engine/Scene/Save/components/TriggerWay';
+import { Col, Form, Row } from 'antd';
+import {
+  BuiltIn,
+  OrgList,
+  TagSelect,
+  UserList,
+} from '@/pages/rule-engine/Scene/Save/action/VariableItems';
+import { InputFile } from '@/pages/rule-engine/Scene/Save/components';
 
 interface MessageContentProps {
   name: number;
   template?: any;
   form: FormInstance;
+  notifyType: string;
+  triggerType: string;
+  configId: string;
 }
 
 const rowGutter = 12;
 
-const BuiltInSelectOptions = {
-  [TriggerWayType.timing]: [
-    { label: '设备名称', value: 'device-name' },
-    { label: '设备ID', value: 'device-id' },
-    { label: '产品名称', value: 'product-name' },
-    { label: '产品ID', value: 'product-id' },
-    { label: '系统时间', value: 'device-name' },
-    { label: '设备名称', value: 'device-name' },
-  ],
-  [TriggerWayType.manual]: [],
-  [TriggerWayType.device]: [],
-};
-
 export default (props: MessageContentProps) => {
-  const [triggerType, setTriggerType] = useState('');
-
-  useEffect(() => {
-    const trigger = props.form.getFieldsValue([['trigger', 'type']]);
-    if (trigger) {
-      setTriggerType(trigger.type);
-    }
-  }, []);
-
-  // const inputNodeByType = useCallback((data: any) => {
-  //   const { actions } = props.form.getFieldsValue([['actions',props.name, 'notify']])
-  //   console.log(actions);
-  //   if (actions && actions[props.name].notify && actions[props.name].notify.variables) {
-  //     const type = actions[props.name].notify.variables[data.id].type
-  //
-  //     if (type === 2) {
-  //       return <Select options={BuiltInSelectOptions[triggerType] || []} style={{ width: '100%'}} />
-  //     }
-  //   }
-  //
-  //   switch (data.type) {
-  //     case 'enum':
-  //       return <Select placeholder={`请选择${data.name}`} style={{ width: '100%' }} />;
-  //     case 'date':
-  //       // @ts-ignore
-  //       return <DatePicker style={{ width: '100%' }} format={data.format || 'YYYY-MM-DD HH:mm:ss'} />;
-  //     case 'number':
-  //       return <InputNumber placeholder={`请输入${data.name}`} style={{ width: '100%' }} />;
-  //     case 'file':
-  //       return <InputFile />;
-  //     default:
-  //       return <Input placeholder={`请输入${data.name}`} />;
-  //   }
-  // }, [triggerType]);
-
   return (
     <>
       {props.template && (
@@ -68,89 +27,38 @@ export default (props: MessageContentProps) => {
           {props.template.variableDefinitions ? (
             <Row gutter={rowGutter}>
               {props.template.variableDefinitions.map((item: any, index: number) => {
+                const type = item.expands?.businessType || item.type;
+                const _name = [props.name, 'notify', 'variables', item.id];
+                let initialValue = undefined;
+                if (type === 'user') {
+                  initialValue = {
+                    source: 'relation',
+                    value: undefined,
+                  };
+                } else if (['date', 'number', 'string'].includes(type)) {
+                  initialValue = {
+                    source: 'fixed',
+                    value: undefined,
+                  };
+                }
                 return (
                   <Col span={12} key={`${item.id}_${index}`}>
-                    <Form.Item label={item.name} style={{ margin: 0 }}>
-                      <ItemGroup>
-                        <Form.Item
-                          name={[props.name, 'notify', 'variables', item.id, 'type']}
-                          initialValue={'1'}
-                        >
-                          <Select
-                            options={[
-                              { label: '手动输入', value: '1' },
-                              { label: '内置参数', value: '2' },
-                            ]}
-                            style={{ width: 120 }}
-                          />
-                        </Form.Item>
-                        <Form.Item
-                          name={[props.name, 'notify', 'variables', item.id, 'value']}
-                          shouldUpdate={(prevValues, curValues) => {
-                            const oldNotify = prevValues.actions[props.name].notify.variables;
-                            const curNotify = curValues.actions[props.name].notify.variables;
-
-                            if (oldNotify && curNotify) {
-                              if (oldNotify[item.id] && curNotify[item.id]) {
-                                return oldNotify[item.id].type !== curNotify[item.id].type;
-                              }
-                            }
-                            return false;
-                          }}
-                        >
-                          {() => {
-                            const { actions } = props.form.getFieldsValue([
-                              ['actions', props.name, 'notify'],
-                            ]);
-                            console.log(actions);
-                            if (
-                              actions &&
-                              actions[props.name].notify &&
-                              actions[props.name].notify.variables
-                            ) {
-                              const type = actions[props.name].notify.variables[item.id].type;
-
-                              if (type === 2) {
-                                return (
-                                  <Select
-                                    options={BuiltInSelectOptions[triggerType] || []}
-                                    style={{ width: '100%' }}
-                                  />
-                                );
-                              }
-                            }
-
-                            switch (item.type) {
-                              case 'enum':
-                                return (
-                                  <Select
-                                    placeholder={`请选择${item.name}`}
-                                    style={{ width: '100%' }}
-                                  />
-                                );
-                              case 'date':
-                                return (
-                                  // @ts-ignore
-                                  <DatePicker
-                                    style={{ width: '100%' }}
-                                    format={item.format || 'YYYY-MM-DD HH:mm:ss'}
-                                  />
-                                );
-                              case 'number':
-                                return (
-                                  <InputNumber
-                                    placeholder={`请输入${item.name}`}
-                                    style={{ width: '100%' }}
-                                  />
-                                );
-                              case 'file':
-                                return <InputFile />;
-                              default:
-                                return <Input placeholder={`请输入${item.name}`} />;
-                            }
-                          }}
-                        </Form.Item>
-                      </ItemGroup>
+                    <Form.Item name={_name} label={item.name} initialValue={initialValue}>
+                      {type === 'user' ? (
+                        <UserList
+                          notifyType={props.notifyType}
+                          configId={props.configId}
+                          type={props.triggerType}
+                        />
+                      ) : type === 'org' ? (
+                        <OrgList notifyType={props.notifyType} configId={props.configId} />
+                      ) : type === 'file' ? (
+                        <InputFile />
+                      ) : type === 'tag' ? (
+                        <TagSelect configId={props.configId} />
+                      ) : (
+                        <BuiltIn type={props.triggerType} data={item} />
+                      )}
                     </Form.Item>
                   </Col>
                 );

+ 40 - 0
src/pages/rule-engine/Scene/Save/action/service.ts

@@ -29,3 +29,43 @@ export const queryProductList = (data?: any) =>
 
 export const queryDeviceSelector = () =>
   request(`${SystemConst.API_BASE}/scene/device-selectors`, { method: 'GET' });
+
+// 内置参数
+export const queryBuiltInParams = (data: any) =>
+  request(`${SystemConst.API_BASE}/scene/parse-variables`, { method: 'POST', data });
+
+// 平台用户
+export const queryPlatformUsers = () =>
+  request(`${SystemConst.API_BASE}/user/_query/no-paging`, {
+    method: 'POST',
+    data: { paging: false, sorts: [{ name: 'name', order: 'asc' }] },
+  });
+
+// 关系用户
+export const queryRelationUsers = () =>
+  request(`${SystemConst.API_BASE}/relation/_query/no-paging`, {
+    method: 'POST',
+    data: { paging: false, sorts: [{ name: 'name', order: 'asc' }] },
+  });
+
+// 钉钉用户
+export const queryDingTalkUsers = (id: string) =>
+  request(`${SystemConst.API_BASE}/notifier/dingtalk/corp/${id}/users`, { method: 'GET' });
+
+// 钉钉部门
+export const queryDingTalkDepartments = (id: string) =>
+  request(`${SystemConst.API_BASE}/notifier/dingtalk/corp/${id}/departments/tree`, {
+    method: 'GET',
+  });
+
+// 微信用户
+export const queryWechatUsers = (id: string) =>
+  request(`${SystemConst.API_BASE}/notifier/wechat/corp/${id}/users`, { method: 'GET' });
+
+// 微信部门
+export const queryWechatDepartments = (id: string) =>
+  request(`${SystemConst.API_BASE}/notifier/wechat/corp/${id}/departments`, { method: 'GET' });
+
+// 根据配置ID获取标签推送
+export const queryTag = (id: string) =>
+  request(`${SystemConst.API_BASE}/notifier/wechat/corp/${id}/tags`, { method: 'GET' });

+ 28 - 0
src/pages/rule-engine/Scene/Save/components/DatePickerFormat/index.tsx

@@ -0,0 +1,28 @@
+import type { DatePickerProps } from 'antd';
+import { DatePicker } from 'antd';
+import moment from 'moment';
+
+interface DatePickerFormat extends Omit<DatePickerProps, 'onChange'> {
+  onChange?: (dateString: string, date: moment.Moment | null) => void;
+}
+
+export default (props: DatePickerFormat) => {
+  const { value, onChange, ...extraProps } = props;
+
+  return (
+    <>
+      {
+        // @ts-ignore
+        <DatePicker
+          {...extraProps}
+          value={typeof value === 'string' ? moment(value) : value}
+          onChange={(date, dateString) => {
+            if (onChange) {
+              onChange(dateString, date);
+            }
+          }}
+        />
+      }
+    </>
+  );
+};

+ 53 - 0
src/pages/rule-engine/Scene/Save/components/InputNumber.tsx

@@ -0,0 +1,53 @@
+import { InputNumber, Select } from 'antd';
+import { useEffect, useState } from 'react';
+
+interface InputNumberProps {
+  value?: { time: number; unit: string };
+  onChange?: (value: { time: number; unit: string }) => void;
+}
+
+export default (props: InputNumberProps) => {
+  const [time, setTime] = useState(props.value?.time || 0);
+  const [unit, setUnit] = useState(props.value?.unit || 'seconds');
+
+  useEffect(() => {
+    setTime(props.value?.time || 0);
+    setUnit(props.value?.unit || 'seconds');
+  }, [props.value]);
+
+  const TimeTypeAfter = (
+    <Select
+      value={unit}
+      options={[
+        { label: '秒', value: 'seconds' },
+        { label: '分', value: 'minutes' },
+        { label: '小时', value: 'hours' },
+      ]}
+      onChange={(key) => {
+        if (props.onChange) {
+          props.onChange({
+            time: time,
+            unit: key,
+          });
+        }
+      }}
+    />
+  );
+
+  return (
+    <InputNumber
+      addonAfter={TimeTypeAfter}
+      style={{ width: 150 }}
+      min={0}
+      max={9999}
+      onChange={(value) => {
+        if (props.onChange) {
+          props.onChange({
+            time: value,
+            unit,
+          });
+        }
+      }}
+    />
+  );
+};

+ 2 - 2
src/pages/rule-engine/Scene/Save/components/InputUpload/index.tsx

@@ -1,5 +1,5 @@
 import { useEffect, useState } from 'react';
-import { Upload, Input, Button } from 'antd';
+import { Button, Input, Upload } from 'antd';
 import { UploadChangeParam } from 'antd/lib/upload/interface';
 import SystemConst from '@/utils/const';
 import Token from '@/utils/token';
@@ -13,7 +13,7 @@ interface InputUploadProps {
 export default (props: InputUploadProps) => {
   const { onChange } = props;
 
-  const [url, setUrl] = useState('');
+  const [url, setUrl] = useState(props.value);
   const [loading, setLoading] = useState<boolean>(false);
 
   useEffect(() => {

+ 7 - 2
src/pages/rule-engine/Scene/Save/components/TimeSelect/index.tsx

@@ -8,8 +8,8 @@ type OptionsItemType = {
 
 interface TimeSelectProps {
   options?: OptionsItemType[];
-  value?: string;
-  onChange?: (value: string[]) => void;
+  value?: number[];
+  onChange?: (value: number[]) => void;
   style?: React.CSSProperties;
 }
 
@@ -39,6 +39,11 @@ export default (props: TimeSelectProps) => {
       value={checkedKeys}
       onChange={onChange}
       style={props.style}
+      maxTagCount={0}
+      placeholder={'请选择时间'}
+      maxTagPlaceholder={(values) => {
+        return <span className={''}>{values.map((item) => item.label).toString()}</span>;
+      }}
       treeData={
         props.options && props.options.length
           ? [{ label: '全部', value: 'all' }, ...props.options]

+ 179 - 34
src/pages/rule-engine/Scene/Save/components/TimingTrigger/index.tsx

@@ -1,45 +1,177 @@
 import { Input, InputNumber, Select, TimePicker } from 'antd';
 import { TimeSelect } from '@/pages/rule-engine/Scene/Save/components';
-import { useState } from 'react';
+import { useCallback, useEffect, useState } from 'react';
+import { omit } from 'lodash';
+import moment from 'moment';
 
-export default () => {
-  const [type1, setType1] = useState(1);
-  const [type2, setType2] = useState(1);
+type TimerType = {
+  trigger: string;
+  cron?: string;
+  when?: number[];
+  mod?: string;
+  period?: {
+    from?: string;
+    to?: string;
+    every?: number;
+    unit?: string;
+  };
+  once?: {
+    time?: string;
+  };
+};
+
+interface TimingTrigger {
+  value?: TimerType;
+  onChange?: (value: TimerType) => void;
+}
+
+enum TriggerEnum {
+  'week' = 'week',
+  'month' = 'month',
+  'cron' = 'cron',
+}
+
+enum PeriodModEnum {
+  'period' = 'period',
+  'once' = 'once',
+}
+
+const DefaultValue = {
+  trigger: TriggerEnum.week,
+  mod: PeriodModEnum.period,
+  period: {},
+};
 
-  const type1Select = async (key: number) => {
-    setType1(key);
-    if (key !== 3) {
-      // TODO 请求后端返回天数
+export default (props: TimingTrigger) => {
+  const [data, setData] = useState<TimerType>(DefaultValue);
+
+  useEffect(() => {
+    setData(props.value || DefaultValue);
+  }, [props.value]);
+
+  const onChange = (newData: TimerType) => {
+    if (props.onChange) {
+      props.onChange(newData);
     }
   };
 
-  const type2Select = (key: number) => {
-    setType2(key);
+  const type1Select = async (key: string) => {
+    if (key !== TriggerEnum.cron) {
+      onChange({
+        trigger: key,
+        mod: PeriodModEnum.period,
+        when: [],
+        period: {
+          unit: 'second',
+        },
+      });
+    } else {
+      onChange({
+        trigger: key,
+        cron: undefined,
+      });
+    }
   };
 
+  const type2Select = useCallback(
+    (key: string) => {
+      if (key === PeriodModEnum.period) {
+        onChange({
+          ...omit(data, 'once'),
+          mod: key,
+          period: {
+            from: undefined,
+            to: undefined,
+            unit: 'second',
+          },
+        });
+      } else {
+        onChange({
+          ...omit(data, 'period'),
+          mod: key,
+          once: {
+            time: undefined,
+          },
+        });
+      }
+    },
+    [data],
+  );
+
   const TimeTypeAfter = (
     <Select
-      defaultValue={'second'}
+      value={data.period?.unit || 'second'}
       options={[
         { label: '秒', value: 'second' },
         { label: '分', value: 'minute' },
         { label: '小时', value: 'hour' },
       ]}
+      onSelect={(key: string) => {
+        onChange({
+          ...data,
+          period: {
+            ...data.period,
+            unit: key,
+          },
+        });
+      }}
     />
   );
 
   const implementNode =
-    type2 === 1 ? (
+    data.mod === PeriodModEnum.period ? (
       <>
-        <TimePicker.RangePicker />
+        <TimePicker.RangePicker
+          format={'hh:mm:ss'}
+          value={
+            data.period?.from
+              ? [moment(data.period?.from, 'hh:mm:ss'), moment(data.period?.to, 'hh:mm:ss')]
+              : undefined
+          }
+          onChange={(_, dateString) => {
+            onChange({
+              ...data,
+              period: {
+                ...data.period,
+                from: dateString[0],
+                to: dateString[1],
+              },
+            });
+          }}
+        />
         <span> 每 </span>
-        <InputNumber addonAfter={TimeTypeAfter} style={{ width: 150 }} min={0} max={9999} />
+        <InputNumber
+          value={data.period?.every}
+          addonAfter={TimeTypeAfter}
+          style={{ width: 150 }}
+          min={0}
+          max={9999}
+          onChange={(e) => {
+            onChange({
+              ...data,
+              period: {
+                ...data.period,
+                every: e,
+              },
+            });
+          }}
+        />
         <span> 执行一次 </span>
       </>
     ) : (
       <>
-        <TimePicker />
-        <InputNumber addonAfter={TimeTypeAfter} style={{ width: 150 }} min={0} max={9999} />
+        <TimePicker
+          format={'hh:mm:ss'}
+          value={data.once?.time ? moment(data.once?.time, 'hh:mm:ss') : undefined}
+          onChange={(_, dateString) => {
+            onChange({
+              ...data,
+              once: {
+                time: dateString,
+              },
+            });
+          }}
+        />
         <span> 执行一次 </span>
       </>
     );
@@ -48,34 +180,47 @@ export default () => {
     <div style={{ display: 'flex', gap: 12, alignItems: 'center' }}>
       <Select
         options={[
-          { label: '按周', value: 1 },
-          { label: '按月', value: 2 },
-          { label: 'cron表达式', value: 3 },
+          { label: '按周', value: TriggerEnum.week },
+          { label: '按月', value: TriggerEnum.month },
+          { label: 'cron表达式', value: TriggerEnum.cron },
         ]}
-        value={type1}
+        value={data.trigger}
         onSelect={type1Select}
         style={{ width: 120 }}
       />
-      {type1 !== 3 ? (
+      {data.trigger !== TriggerEnum.cron ? (
         <>
           <TimeSelect
-            options={[
-              { label: '周一', value: 1 },
-              { label: '周二', value: 2 },
-              { label: '周三', value: 3 },
-              { label: '周四', value: 4 },
-              { label: '周五', value: 5 },
-              { label: '周六', value: 6 },
-              { label: '周末', value: 7 },
-            ]}
-            style={{ width: 150 }}
+            value={data.when}
+            options={
+              data.trigger === TriggerEnum.week
+                ? [
+                    { label: '周一', value: 1 },
+                    { label: '周二', value: 2 },
+                    { label: '周三', value: 3 },
+                    { label: '周四', value: 4 },
+                    { label: '周五', value: 5 },
+                    { label: '周六', value: 6 },
+                    { label: '周末', value: 7 },
+                  ]
+                : new Array(31)
+                    .fill(1)
+                    .map((_, index) => ({ label: index + 1 + '号', value: index + 1 }))
+            }
+            style={{ width: 180 }}
+            onChange={(keys) => {
+              onChange({
+                ...data,
+                when: keys,
+              });
+            }}
           />
           <Select
             options={[
-              { label: '周期执行', value: 1 },
-              { label: '执行一次', value: 2 },
+              { label: '周期执行', value: PeriodModEnum.period },
+              { label: '执行一次', value: PeriodModEnum.once },
             ]}
-            value={type2}
+            value={data.mod}
             style={{ width: 150 }}
             onSelect={type2Select}
           />

+ 5 - 0
src/pages/rule-engine/Scene/Save/components/TriggerWay/index.tsx

@@ -5,6 +5,7 @@ import './index.less';
 interface TriggerWayProps {
   value?: string;
   onChange?: (type: string) => void;
+  onSelect?: (type: string) => void;
 }
 
 export enum TriggerWayType {
@@ -23,6 +24,10 @@ export default (props: TriggerWayProps) => {
   const onSelect = (_type: string) => {
     setType(_type);
 
+    if (props.onSelect) {
+      props.onSelect(_type);
+    }
+
     if (props.onChange) {
       props.onChange(_type);
     }

+ 2 - 0
src/pages/rule-engine/Scene/Save/components/index.ts

@@ -3,3 +3,5 @@ export { default as TimingTrigger } from './TimingTrigger';
 export { default as TriggerWay } from './TriggerWay';
 export { default as ItemGroup } from './ItemGroup';
 export { default as InputFile } from './InputUpload';
+export { default as DatePickerFormat } from './DatePickerFormat';
+export { default as InputNumber } from './InputNumber';

+ 215 - 94
src/pages/rule-engine/Scene/Save/index.tsx

@@ -1,12 +1,34 @@
 import { PageContainer } from '@ant-design/pro-layout';
-import { Button, Card, Form, Input } from 'antd';
+import {
+  Button,
+  Card,
+  Form,
+  Input,
+  InputNumber,
+  message,
+  Radio,
+  Space,
+  Switch,
+  Tooltip,
+} from 'antd';
 import { useLocation } from 'umi';
 import { useEffect, useRef, useState } from 'react';
 import { PermissionButton } from '@/components';
 import ActionItems from './action/action';
-import { PlusOutlined } from '@ant-design/icons';
-import { TriggerWay } from './components';
+import { PlusOutlined, QuestionCircleOutlined } from '@ant-design/icons';
+import { TimingTrigger, TriggerWay } from './components';
+import { TriggerWayType } from './components/TriggerWay';
 import TriggerTerm from '@/pages/rule-engine/Scene/TriggerTerm';
+import TriggerDevice from './trigger';
+import { service } from '../index';
+
+type ShakeLimitType = {
+  enabled: boolean;
+  groupType?: string;
+  time?: number;
+  threshold?: number;
+  alarmFirst?: boolean;
+};
 
 export default () => {
   const location = useLocation();
@@ -14,6 +36,14 @@ export default () => {
   const triggerRef = useRef<any>();
 
   const { getOtherPermission } = PermissionButton.usePermission('rule-engine/Scene');
+  const [triggerType, setTriggerType] = useState('');
+  // const [triggerValue, setTriggerValue] = useState<any>();
+  const [parallel, setParallel] = useState(false); // 是否并行
+  const [shakeLimit, setShakeLimit] = useState<ShakeLimitType>({
+    enabled: false,
+    alarmFirst: true,
+  });
+  const [requestParams, setRequestParams] = useState<any>(undefined);
 
   const getDetail = async () => {
     // TODO 回显数据
@@ -29,73 +59,162 @@ export default () => {
 
   const saveData = async () => {
     const formData = await form.validateFields();
+    let triggerData = undefined;
     // 获取触发条件数据
-    const triggerData = await triggerRef.current.getTriggerForm();
-    console.log(JSON.stringify(triggerData), 'trigger');
+    if (triggerRef.current) {
+      triggerData = await triggerRef.current.getTriggerForm();
+      console.log(JSON.stringify(triggerData), 'trigger');
+      if (!triggerData) {
+        return;
+      }
+      formData.terms = triggerData.trigger;
+    }
     console.log(formData);
+    if (formData) {
+      const resp = formData.id ? await service.update(formData) : await service.save(formData);
+      if (resp.status === 200) {
+        message.success('操作成功');
+      } else {
+        message.error(resp.message);
+      }
+    }
   };
 
-  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: [],
-    },
-  };
+  const AntiShake = (
+    <Space>
+      <span>触发条件</span>
+      <Switch
+        checked={shakeLimit.enabled}
+        checkedChildren="开启防抖"
+        unCheckedChildren="关闭防抖"
+        onChange={(e) => {
+          setShakeLimit({
+            ...shakeLimit,
+            enabled: e,
+          });
+          form.setFieldsValue({ shakeLimit });
+        }}
+      />
+      {shakeLimit.enabled && (
+        <>
+          <InputNumber
+            value={shakeLimit.time}
+            onChange={(e: number) => {
+              setShakeLimit({
+                ...shakeLimit,
+                time: e,
+              });
+              form.setFieldsValue({ shakeLimit });
+            }}
+          />
+          <span> 秒内发生 </span>
+          <InputNumber
+            value={shakeLimit.threshold}
+            onChange={(e: number) => {
+              setShakeLimit({
+                ...shakeLimit,
+                threshold: e,
+              });
+              form.setFieldsValue({ shakeLimit });
+            }}
+          />
+          <span>次及以上时,处理</span>
+          <Radio.Group
+            value={shakeLimit.alarmFirst}
+            options={[
+              { label: '第一次', value: true },
+              { label: '最后一次', value: false },
+            ]}
+            optionType="button"
+            onChange={(e) => {
+              console.log(e);
+              setShakeLimit({
+                ...shakeLimit,
+                alarmFirst: e.target.value,
+              });
+              form.setFieldsValue({ shakeLimit });
+            }}
+          ></Radio.Group>
+        </>
+      )}
+    </Space>
+  );
 
   return (
     <PageContainer>
       <Card>
-        <Form form={form} colon={false} layout={'vertical'} preserve={false}>
+        <Form
+          form={form}
+          colon={false}
+          layout={'vertical'}
+          preserve={false}
+          onValuesChange={(changeValue, allValues) => {
+            if (allValues.trigger?.device?.selectorValues) {
+              setRequestParams({ trigger: allValues.trigger });
+            } else {
+              setRequestParams(undefined);
+            }
+          }}
+        >
           <Form.Item name={'name'} label={'名称'}>
             <Input placeholder={'请输入名称'} />
           </Form.Item>
           <Form.Item label={'触发方式'}>
             <Form.Item name={['trigger', 'type']}>
-              <TriggerWay />
+              <TriggerWay onSelect={setTriggerType} />
+            </Form.Item>
+            {triggerType === TriggerWayType.timing && (
+              <Form.Item name={['trigger', 'timer']}>
+                <TimingTrigger />
+              </Form.Item>
+            )}
+            {triggerType === TriggerWayType.device && <TriggerDevice form={form} />}
+          </Form.Item>
+          {triggerType === TriggerWayType.device &&
+          requestParams &&
+          requestParams.trigger?.device?.productId ? (
+            <Form.Item label={AntiShake}>
+              <TriggerTerm
+                ref={triggerRef}
+                params={requestParams}
+                // value={triggerValue}
+              />
             </Form.Item>
+          ) : null}
+          <Form.Item hidden name={'parallel'} initialValue={false}>
+            <Input />
           </Form.Item>
-          <Form.Item label={'执行动作'}>
+          <Form.Item
+            label={
+              <>
+                <span>
+                  执行动作<span style={{ color: 'red', margin: '0 4px' }}>*</span>
+                </span>
+                <Tooltip
+                  title={
+                    <div>
+                      <div>串行:满足所有执行条件才会触发执行动作</div>
+                      <div>并行:满足任意条件时会触发执行动作</div>
+                    </div>
+                  }
+                >
+                  <QuestionCircleOutlined style={{ margin: '0 8px', fontSize: 14 }} />
+                </Tooltip>
+                <Radio.Group
+                  value={parallel}
+                  options={[
+                    { label: '串行', value: false },
+                    { label: '并行', value: true },
+                  ]}
+                  optionType="button"
+                  onChange={(e) => {
+                    setParallel(e.target.value);
+                    form.setFieldsValue({ parallel: e.target.value });
+                  }}
+                ></Radio.Group>
+              </>
+            }
+          >
             <Form.List name="actions">
               {(fields, { add, remove }) => (
                 <>
@@ -104,8 +223,9 @@ export default () => {
                       key={key}
                       form={form}
                       restField={restField}
-                      onRemove={() => remove(name)}
                       name={name}
+                      triggerType={triggerType}
+                      onRemove={() => remove(name)}
                     />
                   ))}
                   <Form.Item>
@@ -117,48 +237,49 @@ export default () => {
               )}
             </Form.List>
           </Form.Item>
+          <Form.Item label={'说明'} name={'description'}>
+            <Input.TextArea showCount maxLength={200} placeholder={'请输入说明'} />
+          </Form.Item>
+          <Form.Item hidden name={'shakeLimit'}>
+            <Input />
+          </Form.Item>
+          <Form.Item hidden name={'id'}>
+            <Input />
+          </Form.Item>
         </Form>
         <PermissionButton isPermission={getOtherPermission(['add', 'update'])} onClick={saveData}>
           保存
         </PermissionButton>
-        <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>
-      <Card>
-        <TriggerTerm
-          ref={triggerRef}
-          params={requestParams}
-          value={triggerValue}
-          onChange={console.log}
-        />
+        {/*<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>
   );

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

@@ -1,10 +1,266 @@
-import { Form } from 'antd';
-import TriggerWay from '../components/TriggerWay';
+import { useCallback, useEffect, useState } from 'react';
+import type { FormInstance } from 'antd';
+import { Col, Form, Row, Select, Space, TreeSelect } from 'antd';
+import { TimingTrigger } from '@/pages/rule-engine/Scene/Save/components';
+import { getProductList } from '@/pages/rule-engine/Scene/Save/action/device/service';
+import { queryOrgTree, querySelector } from '@/pages/rule-engine/Scene/Save/trigger/service';
+import Device from '@/pages/rule-engine/Scene/Save/action/device/deviceModal';
+import FunctionCall from '@/pages/rule-engine/Scene/Save/action/device/functionCall';
+import Operation from './operation';
+
+interface TriggerProps {
+  value?: string;
+  onChange?: (type: string) => void;
+  form?: FormInstance;
+}
+
+enum OperatorEnum {
+  'online' = 'online',
+  'offline' = 'offline',
+  'reportEvent' = 'reportEvent',
+  'reportProperty' = 'reportProperty',
+  'readProperty' = 'readProperty',
+  'writeProperty' = 'writeProperty',
+  'invokeFunction' = 'invokeFunction',
+}
+
+export default (props: TriggerProps) => {
+  const [productList, setProductList] = useState<any[]>([]);
+  const [productId, setProductId] = useState('');
+  const [selector, setSelector] = useState('fixed');
+
+  const [selectorOptions, setSelectorOptions] = useState<any[]>([]);
+  const [operation, setOperation] = useState<string | undefined>(undefined);
+  const [operatorOptions, setOperatorOptions] = useState<any[]>([]);
+
+  const [properties, setProperties] = useState<any[]>([]); // 属性
+  const [events, setEvents] = useState([]); // 事件
+  const [functions, setFunctions] = useState([]); // 功能列表
+
+  const [functionItem, setFunctionItem] = useState<any[]>([]); // 单个功能-属性列表
+  const [orgTree, setOrgTree] = useState<any>([]);
+
+  const getProducts = async () => {
+    const resp = await getProductList({ paging: false });
+    if (resp && resp.status === 200) {
+      setProductList(resp.result);
+    }
+  };
+
+  const getSelector = () => {
+    querySelector().then((resp) => {
+      if (resp && resp.status === 200) {
+        setSelectorOptions(resp.result);
+      }
+    });
+  };
+
+  const getOrgTree = useCallback(() => {
+    queryOrgTree(productId).then((resp) => {
+      if (resp && resp.status === 200) {
+        setOrgTree(resp.result);
+      }
+    });
+  }, [queryOrgTree]);
+
+  const handleMetadata = (metadata?: string) => {
+    try {
+      const metadataObj = JSON.parse(metadata || '{}');
+      let newOperator: any[] = [
+        { label: '设备上线', value: OperatorEnum.online },
+        { label: '设备离线', value: OperatorEnum.offline },
+      ];
+      if (metadataObj.properties && metadataObj.properties.length) {
+        newOperator = [
+          ...newOperator,
+          { label: '属性上报', value: OperatorEnum.reportProperty },
+          { label: '属性读取', value: OperatorEnum.readProperty },
+          { label: '修改属性', value: OperatorEnum.writeProperty },
+        ];
+        setProperties(metadataObj.properties);
+      }
+      if (metadataObj.events && metadataObj.events.length) {
+        newOperator = [...newOperator, { label: '事件上报', value: OperatorEnum.reportEvent }];
+        setEvents(metadataObj.events);
+      }
+      if (metadataObj.functions && metadataObj.functions.length) {
+        newOperator = [...newOperator, { label: '功能调用', value: OperatorEnum.invokeFunction }];
+        setFunctions(metadataObj.functions);
+      }
+      setOperatorOptions(newOperator);
+    } catch (err) {
+      console.warn('handleMetadata === ', err);
+    }
+  };
+
+  useEffect(() => {
+    getProducts();
+    getSelector();
+  }, []);
 
-export default () => {
   return (
-    <Form.Item>
-      <TriggerWay />
-    </Form.Item>
+    <div>
+      <Row>
+        <Col span={24}>
+          <Space>
+            <Form.Item name={['trigger', 'device', 'productId']}>
+              <Select
+                options={productList}
+                placeholder={'请选择产品'}
+                style={{ width: 220 }}
+                listHeight={220}
+                onChange={(key: any, node: any) => {
+                  props.form?.resetFields([['trigger', 'device', 'selector']]);
+                  props.form?.resetFields([['trigger', 'device', 'selectorValues']]);
+                  props.form?.resetFields([['trigger', 'device', 'operation', 'operator']]);
+                  props.form?.resetFields([['trigger', 'device', 'operation', 'operator']]);
+                  setOperation(undefined);
+                  setProductId(key);
+                  handleMetadata(node.metadata);
+                  if (selector === 'org') {
+                    getOrgTree();
+                  }
+                }}
+                fieldNames={{ label: 'name', value: 'id' }}
+              />
+            </Form.Item>
+            <Form.Item name={['trigger', 'device', 'selector']} initialValue={'fixed'}>
+              <Select
+                options={selectorOptions}
+                fieldNames={{ label: 'name', value: 'id' }}
+                onSelect={(key: string) => {
+                  if (key === 'org') {
+                    getOrgTree();
+                  }
+                  setSelector(key);
+                }}
+                style={{ width: 120 }}
+              />
+            </Form.Item>
+            {selector === 'fixed' && (
+              <Form.Item name={['trigger', 'device', 'selectorValues']}>
+                <Device productId={productId} />
+              </Form.Item>
+            )}
+            {selector === 'org' && (
+              <Form.Item name={['trigger', 'device', 'selectorValues']}>
+                <TreeSelect
+                  treeData={orgTree}
+                  fieldNames={{ label: 'name', value: 'id' }}
+                  placeholder={'请选择部门'}
+                  style={{ width: 300 }}
+                />
+              </Form.Item>
+            )}
+            {functions.length || events.length || properties.length ? (
+              <Form.Item name={['trigger', 'device', 'operation', 'operator']}>
+                <Select
+                  placeholder={'请选择触发类型'}
+                  options={operatorOptions}
+                  style={{ width: 140 }}
+                  onSelect={setOperation}
+                />
+              </Form.Item>
+            ) : null}
+          </Space>
+        </Col>
+        {operation === OperatorEnum.invokeFunction || operation === OperatorEnum.writeProperty ? (
+          <Col>
+            <Form.Item name={['trigger', 'device', 'operation', 'timer']}>
+              <TimingTrigger />
+            </Form.Item>
+          </Col>
+        ) : null}
+      </Row>
+      {operation === OperatorEnum.invokeFunction && (
+        <>
+          <Row>
+            <Col span={12}>
+              <Form.Item name={['trigger', 'device', 'operation', 'functionId']}>
+                <Select
+                  options={functions}
+                  fieldNames={{
+                    label: 'name',
+                    value: 'id',
+                  }}
+                  placeholder={'请选择功能'}
+                  onSelect={(_: any, data: any) => {
+                    const _properties = data.valueType ? data.valueType.properties : data.inputs;
+                    const array = [];
+                    for (const datum of _properties) {
+                      array.push({
+                        id: datum.id,
+                        name: datum.name,
+                        type: datum.valueType ? datum.valueType.type : '-',
+                        format: datum.valueType ? datum.valueType.format : undefined,
+                        options: datum.valueType ? datum.valueType.elements : undefined,
+                        value: undefined,
+                      });
+                    }
+                    setFunctionItem(array);
+                  }}
+                />
+              </Form.Item>
+            </Col>
+            <Col span={8}>
+              <span style={{ margin: '0 12px', lineHeight: '32px' }}>
+                定时调用所选功能,功能返回值用于条件配置
+              </span>
+            </Col>
+          </Row>
+          <Row>
+            <Col span={24}>
+              <Form.Item name={['trigger', 'device', 'operation', 'functionParameters']}>
+                <FunctionCall functionData={functionItem} />
+              </Form.Item>
+            </Col>
+          </Row>
+        </>
+      )}
+      {operation === OperatorEnum.writeProperty && (
+        <Row>
+          <Col span={24}>
+            <Form.Item name={['trigger', 'device', 'operation', 'writeProperties']}>
+              <Operation
+                propertiesList={properties.filter((item) => {
+                  if (item.expands) {
+                    return item.expands.type ? item.expands.type.includes('write') : false;
+                  }
+                  return false;
+                })}
+              />
+            </Form.Item>
+          </Col>
+        </Row>
+      )}
+      {operation === OperatorEnum.readProperty && (
+        <Row>
+          <Col>
+            <Form.Item name={['trigger', 'device', 'operation', 'readProperties']} noStyle>
+              <Select
+                mode={'multiple'}
+                options={properties.filter((item) => {
+                  if (item.expands) {
+                    return item.expands.type ? item.expands.type.includes('read') : false;
+                  }
+                  return false;
+                })}
+                maxTagCount={0}
+                maxTagPlaceholder={(values) => {
+                  return (
+                    <div style={{ maxWidth: 'calc(100% - 8px)' }}>
+                      {values.map((item) => item.label).toString()}
+                    </div>
+                  );
+                }}
+                style={{ width: 300 }}
+                fieldNames={{ label: 'name', value: 'id' }}
+              />
+            </Form.Item>
+            <span style={{ margin: '0 12px' }}>定时读取所选属性值,用于条件配置</span>
+          </Col>
+        </Row>
+      )}
+    </div>
   );
 };

+ 81 - 0
src/pages/rule-engine/Scene/Save/trigger/operation.tsx

@@ -0,0 +1,81 @@
+import { Col, Row, Select } from 'antd';
+import { useEffect, useState } from 'react';
+import FunctionCall from '@/pages/rule-engine/Scene/Save/action/device/functionCall';
+
+interface OperatorProps {
+  propertiesList?: any[];
+  value?: any;
+  onChange?: (value: any) => void;
+}
+
+export default (props: OperatorProps) => {
+  const [data, setData] = useState<any>({});
+  const [key, setKey] = useState('');
+  const [propertiesItem, setPropertiesItem] = useState<any[]>([]);
+
+  const objToArray = (_data: any) => {
+    return Object.keys(_data).map((item) => {
+      return { id: item, value: _data[item] };
+    });
+  };
+
+  useEffect(() => {
+    if (props.value) {
+      const _key = Object.keys(props.value)[0];
+      setKey(_key);
+      setData(objToArray(props.value[_key]));
+    } else {
+      setData({});
+      setKey('');
+    }
+  }, [props.value]);
+
+  return (
+    <Row>
+      <Col span={24}>
+        <Select
+          options={props.propertiesList || []}
+          fieldNames={{
+            label: 'name',
+            value: 'id',
+          }}
+          style={{ width: 300 }}
+          placeholder={'请选择属性'}
+          onSelect={(id: any, _data: any) => {
+            setPropertiesItem([
+              {
+                id: _data.id,
+                name: _data.name,
+                type: _data.valueType ? _data.valueType.type : '-',
+                format: _data.valueType ? _data.valueType.format : undefined,
+                options: _data.valueType ? _data.valueType.elements : undefined,
+                value: undefined,
+              },
+            ]);
+            if (props.onChange) {
+              props.onChange({ [id]: {} });
+            }
+          }}
+        />
+        <span style={{ margin: '0 12px', lineHeight: '32px' }}>
+          定时调用所选属性,修改后的属性值用于条件配置
+        </span>
+      </Col>
+      {key && (
+        <Col span={24}>
+          <FunctionCall
+            value={[{ id: key, value: data[key] }]}
+            functionData={propertiesItem}
+            onChange={(value) => {
+              if (props.onChange) {
+                props.onChange({
+                  [value[0].name]: value[0].value,
+                });
+              }
+            }}
+          />
+        </Col>
+      )}
+    </Row>
+  );
+};

+ 27 - 0
src/pages/rule-engine/Scene/Save/trigger/service.ts

@@ -0,0 +1,27 @@
+import { request } from '@@/plugin-request/request';
+import SystemConst from '@/utils/const';
+
+export const querySelector = () =>
+  request(`${SystemConst.API_BASE}/scene/device-selectors`, {
+    method: 'GET',
+  });
+
+export const queryOrgTree = (id: string) =>
+  request(`${SystemConst.API_BASE}/organization/_all/tree`, {
+    method: 'POST',
+    data: {
+      paging: false,
+      sorts: [{ name: 'sortIndex', order: 'asc' }],
+      terms: [
+        {
+          column: 'id',
+          termType: 'assets-dim',
+          value: {
+            assetType: 'product',
+            assetIds: [id],
+            dimensionType: 'org',
+          },
+        },
+      ],
+    },
+  });

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

@@ -82,7 +82,7 @@ const TriggerTerm = (props: Props, ref: any) => {
             const treeValue = treeFilter(_data, params, 'column');
             // 找到
             const target =
-              treeValue && treeValue[0].children
+              treeValue && treeValue[0] && treeValue[0].children
                 ? treeValue[0]?.children.find((item) => item.column === params)
                 : treeValue[0];
 

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

@@ -3,7 +3,13 @@ import React, { useRef, useState } from 'react';
 import type { ActionType, ProColumns } from '@jetlinks/pro-table';
 import type { SceneItem } from '@/pages/rule-engine/Scene/typings';
 import { Badge, message } from 'antd';
-import { EditOutlined, PlayCircleOutlined, PlusOutlined, StopOutlined } from '@ant-design/icons';
+import {
+  DeleteOutlined,
+  EditOutlined,
+  PlayCircleOutlined,
+  PlusOutlined,
+  StopOutlined,
+} from '@ant-design/icons';
 import { useIntl } from '@@/plugin-locale/localeExports';
 import { PermissionButton, ProTableCard } from '@/components';
 import { statusMap } from '@/pages/device/Instance';
@@ -13,7 +19,7 @@ import Service from './service';
 import { useHistory } from 'umi';
 import { getMenuPathByCode } from '@/utils/menu';
 
-export const service = new Service('rule-engine/scene');
+export const service = new Service('scene');
 
 const Scene = () => {
   const intl = useIntl();
@@ -30,7 +36,7 @@ const Scene = () => {
         style={{ padding: 0 }}
         isPermission={permission.update}
         tooltip={
-          type === 'table'
+          type !== 'table'
             ? {
                 title: intl.formatMessage({
                   id: 'pages.data.option.edit',
@@ -41,7 +47,7 @@ const Scene = () => {
         }
       >
         <EditOutlined />
-        {type === 'table' &&
+        {type !== 'table' &&
           intl.formatMessage({
             id: 'pages.data.option.edit',
             defaultMessage: '编辑',
@@ -55,36 +61,54 @@ const Scene = () => {
         popConfirm={{
           title: intl.formatMessage({
             id: `pages.data.option.${
-              record.state.value !== 'notActive' ? 'disabled' : 'enabled'
+              record.state.value === 'started' ? 'disabled' : 'enabled'
             }.tips`,
             defaultMessage: '确认禁用?',
           }),
           onConfirm: async () => {
-            message.success(
-              intl.formatMessage({
-                id: 'pages.data.option.success',
-                defaultMessage: '操作成功!',
-              }),
-            );
-            actionRef.current?.reload();
+            if (record.state.value !== 'started') {
+              const resp = await service.startScene(record.id);
+              if (resp.status === 200) {
+                message.success(
+                  intl.formatMessage({
+                    id: 'pages.data.option.success',
+                    defaultMessage: '操作成功!',
+                  }),
+                );
+                actionRef.current?.reload();
+              }
+            } else {
+              const resp = await service.stopScene(record.id);
+              if (resp.status === 200) {
+                message.success(
+                  intl.formatMessage({
+                    id: 'pages.data.option.success',
+                    defaultMessage: '操作成功!',
+                  }),
+                );
+                actionRef.current?.reload();
+              }
+            }
           },
         }}
         tooltip={
-          type === 'table'
+          type !== 'table'
             ? {
                 title: intl.formatMessage({
-                  id: 'pages.data.option.edit',
-                  defaultMessage: '编辑',
+                  id: `pages.data.option.${
+                    record.state.value === 'started' ? 'disabled' : 'enabled'
+                  }`,
+                  defaultMessage: '启用',
                 }),
               }
             : undefined
         }
       >
-        {record.state.value !== 'notActive' ? <StopOutlined /> : <PlayCircleOutlined />}
-        {type === 'table' &&
+        {record.state.value === 'started' ? <StopOutlined /> : <PlayCircleOutlined />}
+        {type !== 'table' &&
           intl.formatMessage({
-            id: `pages.data.option.${record.state.value !== 'notActive' ? 'disabled' : 'enabled'}`,
-            defaultMessage: record.state.value !== 'notActive' ? '禁用' : '启用',
+            id: `pages.data.option.${record.state.value === 'started' ? 'disabled' : 'enabled'}`,
+            defaultMessage: record.state.value === 'started' ? '禁用' : '启用',
           })}
       </PermissionButton>,
       <PermissionButton
@@ -95,24 +119,25 @@ const Scene = () => {
         popConfirm={{
           title: intl.formatMessage({
             id:
-              record.state.value === 'notActive'
+              record.state.value === 'started'
                 ? 'pages.data.option.remove.tips'
                 : 'pages.device.instance.deleteTip',
           }),
+          disabled: record.state.value === 'started',
           onConfirm: async () => {},
         }}
         tooltip={
-          type === 'table'
+          type !== 'table'
             ? {
                 title: intl.formatMessage({
-                  id: 'pages.data.option.edit',
-                  defaultMessage: '编辑',
+                  id: 'pages.device.instance.deleteTip',
+                  defaultMessage: '删除',
                 }),
               }
             : undefined
         }
       >
-        <EditOutlined />
+        <DeleteOutlined />
         {type === 'table' &&
           intl.formatMessage({
             id: 'pages.data.option.edit',

+ 3 - 1
src/pages/rule-engine/Scene/service.ts

@@ -3,7 +3,9 @@ import BaseService from '@/utils/BaseService';
 import type { SceneItem } from '@/pages/rule-engine/Scene/typings';
 
 class Service extends BaseService<SceneItem> {
-  start = (id: string) => request(`${this.uri}/${id}`, { methods: 'GET' });
+  startScene = (id: string) => request(`${this.uri}/${id}/_enable`, { method: 'PUT' });
+
+  stopScene = (id: string) => request(`${this.uri}/${id}/_disable`, { method: 'PUT' });
 
   getParseTerm = (data: Record<string, any>) =>
     request(`${this.uri}/parse-term-column`, {

+ 1 - 1
src/pages/system/Department/Assets/product/bind.tsx

@@ -35,7 +35,7 @@ const Bind = observer((props: Props) => {
       dataIndex: 'name',
       title: intl.formatMessage({
         id: 'pages.table.name',
-        defaultMessage: '名',
+        defaultMessage: '名',
       }),
       search: {
         transform: (value) => ({ name$LIKE: value }),

+ 43 - 1
src/pages/system/Relationship/Save/index.tsx

@@ -1,6 +1,6 @@
 import { useIntl } from 'umi';
 import type { Field } from '@formily/core';
-import { createForm } from '@formily/core';
+import { createForm, onFieldInputValueChange, onFieldValueChange } from '@formily/core';
 import { createSchemaField } from '@formily/react';
 import React from 'react';
 import * as ICONS from '@ant-design/icons';
@@ -39,6 +39,16 @@ const Save = (props: Props) => {
     );
   };
 
+  const _validator = async (object: string, target: string, relation: string) => {
+    if (!relation || !target || !object) return;
+    const resp = await service.validator({
+      relation,
+      objectType: JSON.parse(object).objectType,
+      targetType: JSON.parse(target).targetType,
+    });
+    return resp?.result.passed;
+  };
+
   const form = createForm({
     validateFirst: true,
     initialValues: {
@@ -56,6 +66,38 @@ const Save = (props: Props) => {
           })
         : undefined,
     },
+    effects() {
+      onFieldInputValueChange('relation', async (field, f1) => {
+        const relation = field.value;
+        const target = (field.query('target').take() as Field).value;
+        const object = (field.query('object').take() as Field).value;
+        if (!relation || !target || !object) return;
+        const temp = await _validator(object, target, relation);
+        f1.setFieldState('relation', (state) => {
+          state.selfErrors = !temp ? ['关系标识已存在'] : undefined;
+        });
+      });
+      onFieldValueChange('target', async (field, f1) => {
+        const target = field.value;
+        const relation = (field.query('relation').take() as Field).value;
+        const object = (field.query('object').take() as Field).value;
+        if (!relation || !target || !object) return;
+        const temp = await _validator(object, target, relation);
+        f1.setFieldState('relation', (state) => {
+          state.selfErrors = !temp ? ['关系标识已存在'] : undefined;
+        });
+      });
+      onFieldValueChange('object', async (field, f1) => {
+        const object = field.value;
+        const target = (field.query('target').take() as Field).value;
+        const relation = (field.query('relation').take() as Field).value;
+        if (!relation || !target || !object) return;
+        const temp = await _validator(object, target, relation);
+        f1.setFieldState('relation', (state) => {
+          state.selfErrors = !temp ? ['关系标识已存在'] : undefined;
+        });
+      });
+    },
   });
 
   const SchemaField = createSchemaField({

+ 6 - 0
src/pages/system/Relationship/service.ts

@@ -7,6 +7,12 @@ class Service extends BaseService<ReationItem> {
     request(`/${SystemConst.API_BASE}/relation/types`, {
       method: 'GET',
     });
+
+  validator = (params: any) =>
+    request(`/${SystemConst.API_BASE}/relation/_validate?`, {
+      method: 'GET',
+      params,
+    });
 }
 
 export default Service;