Pārlūkot izejas kodu

feat: 新增设备触发

xieyonghong 3 gadi atpakaļ
vecāks
revīzija
594aec428b

BIN
public/images/scene/trigger-device-all.png


BIN
public/images/scene/trigger-device-org.png


+ 1 - 1
src/pages/rule-engine/Scene/Save/components/Buttons/index.less

@@ -5,7 +5,7 @@
   cursor: pointer;
 
   .rule-button {
-    display: inline-block;
+    // display: inline-block;
     padding: 6px 20px;
     font-size: 14px;
     line-height: 22px;

+ 55 - 0
src/pages/rule-engine/Scene/Save/device/TopCard.tsx

@@ -0,0 +1,55 @@
+import classNames from 'classnames';
+import { useEffect, useState } from 'react';
+import './index.less';
+
+interface Props {
+  typeList: any[];
+  value?: string;
+  className?: string;
+  onChange?: (type: string) => void;
+  onSelect?: (type: string) => void;
+  disabled?: boolean;
+}
+
+const TopCard = (props: Props) => {
+  const [type, setType] = useState(props.value || '');
+
+  useEffect(() => {
+    setType(props.value || '');
+  }, [props.value]);
+
+  const onSelect = (_type: string) => {
+    if (!props.disabled) {
+      setType(_type);
+
+      if (props.onChange) {
+        props.onChange(_type);
+      }
+    }
+  };
+
+  return (
+    <div className={classNames('trigger-way-warp', props.className, { disabled: props.disabled })}>
+      {props.typeList.map((item) => (
+        <div
+          key={item.value}
+          className={classNames('trigger-way-item', {
+            active: type === item.value,
+          })}
+          onClick={() => {
+            onSelect(item.value);
+          }}
+        >
+          <div className={'way-item-title'}>
+            <p>{item.label}</p>
+            <span>{item.tip}</span>
+          </div>
+          <div className={'way-item-image'}>
+            <img width={48} src={item.image} />
+          </div>
+        </div>
+      ))}
+    </div>
+  );
+};
+export default TopCard;

+ 147 - 0
src/pages/rule-engine/Scene/Save/device/addModel.tsx

@@ -0,0 +1,147 @@
+import { Modal, Button, Steps } from 'antd';
+import { observer } from '@formily/react';
+import { model } from '@formily/reactive';
+import { useEffect } from 'react';
+import { onlyMessage } from '@/utils/util';
+import type { TriggerDevice } from '@/pages/rule-engine/Scene/typings';
+import Product from './product';
+import Device from './device';
+
+interface AddProps {
+  value?: any;
+  onCancel?: () => void;
+  onSave?: (data: any) => void;
+}
+
+interface DeviceModelProps extends Partial<TriggerDevice> {
+  steps: { key: string; title: string }[];
+  stepNumber: number;
+  productId: string;
+  productDetail: any;
+  deviceId: string;
+  orgId: string;
+}
+
+export const DeviceModel = model<DeviceModelProps>({
+  steps: [
+    {
+      key: 'product',
+      title: '选择产品',
+    },
+    {
+      key: 'device',
+      title: '选择设备',
+    },
+    {
+      key: 'type',
+      title: '触发类型',
+    },
+  ],
+  stepNumber: 0,
+  productId: '',
+  productDetail: {},
+  deviceId: '',
+  orgId: '',
+  selector: 'custom',
+});
+
+export default observer((props: AddProps) => {
+  const prev = () => {
+    DeviceModel.stepNumber -= 1;
+  };
+
+  const next = async () => {
+    if (DeviceModel.stepNumber === 0) {
+      if (DeviceModel.productId) {
+        DeviceModel.stepNumber = 1;
+      } else {
+        onlyMessage('请选择产品', 'error');
+      }
+    } else if (DeviceModel.stepNumber === 1) {
+      if (DeviceModel.selector === 'custom' && !DeviceModel.selectorValues?.length) {
+        onlyMessage('请选择设备', 'error');
+        return;
+      } else if (DeviceModel.selector && !DeviceModel.selectorValues?.length) {
+        onlyMessage('请选择部门', 'error');
+        return;
+      }
+      DeviceModel.stepNumber = 2;
+    } else if (DeviceModel.stepNumber === 2) {
+    }
+  };
+
+  const renderComponent = (type: string) => {
+    switch (type) {
+      case 'device':
+        return <Device />;
+      case 'type':
+        return;
+      default:
+        return <Product />;
+    }
+  };
+
+  useEffect(() => {
+    if (props.value) {
+      // TODO 处理回显数据
+    }
+  }, [props.value]);
+
+  return (
+    <Modal
+      visible
+      title="执行规则"
+      width={800}
+      onCancel={() => {
+        props.onCancel?.();
+        DeviceModel.stepNumber = 0;
+      }}
+      footer={
+        <div className="steps-action">
+          {DeviceModel.stepNumber === 0 && (
+            <Button
+              onClick={() => {
+                props.onCancel?.();
+                DeviceModel.stepNumber = 0;
+              }}
+            >
+              取消
+            </Button>
+          )}
+          {DeviceModel.stepNumber > 0 && (
+            <Button style={{ margin: '0 8px' }} onClick={() => prev()}>
+              上一步
+            </Button>
+          )}
+          {DeviceModel.stepNumber < DeviceModel.steps.length - 1 && (
+            <Button
+              type="primary"
+              onClick={() => {
+                next();
+              }}
+            >
+              下一步
+            </Button>
+          )}
+          {DeviceModel.stepNumber === DeviceModel.steps.length - 1 && (
+            <Button
+              type="primary"
+              onClick={() => {
+                next();
+              }}
+            >
+              确定
+            </Button>
+          )}
+        </div>
+      }
+    >
+      <div className="steps-steps">
+        <Steps current={DeviceModel.stepNumber} items={DeviceModel.steps} />
+      </div>
+      <div className="steps-content">
+        {renderComponent(DeviceModel.steps[DeviceModel.stepNumber]?.key)}
+      </div>
+    </Modal>
+  );
+});

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

@@ -0,0 +1,60 @@
+import { useEffect } from 'react';
+import { observer } from '@formily/reactive-react';
+import { Form } from 'antd';
+import TopCard from './TopCard';
+import { DeviceModel } from './addModel';
+import DeviceList from './deviceList';
+import OrgList from './org';
+
+const TypeList = [
+  {
+    label: '自定义',
+    value: 'custom',
+    image: require('/public/images/scene/device-custom.png'),
+    tip: '自定义选择当前产品下的任意设备',
+  },
+  {
+    label: '全部',
+    value: 'all',
+    image: require('/public/images/scene/trigger-device-all.png'),
+    tip: '产品下的所有设备',
+  },
+  {
+    label: '按组织',
+    value: 'org',
+    image: require('/public/images/scene/trigger-device-org.png'),
+    tip: '选择产品下归属于具体组织的设备',
+  },
+];
+
+export default observer(() => {
+  const [form] = Form.useForm();
+
+  const selector = Form.useWatch('type', form);
+
+  useEffect(() => {
+    form.setFieldsValue({ type: DeviceModel.selector });
+  }, [DeviceModel.selector]);
+
+  const contentRender = (type?: string) => {
+    switch (type) {
+      case 'custom':
+        return <DeviceList />;
+      case 'org':
+        return <OrgList />;
+      default:
+        return <></>;
+    }
+  };
+
+  return (
+    <div>
+      <Form form={form} layout={'vertical'}>
+        <Form.Item name="selector" label="选择方式" required>
+          <TopCard typeList={TypeList} />
+        </Form.Item>
+      </Form>
+      {contentRender(selector)}
+    </div>
+  );
+});

+ 277 - 0
src/pages/rule-engine/Scene/Save/device/deviceList.tsx

@@ -0,0 +1,277 @@
+import { ProTableCard } from '@/components';
+import SearchComponent from '@/components/SearchComponent';
+import type { DeviceInstance } from '@/pages/device/Instance/typings';
+import { useRef, useState } from 'react';
+import type { ActionType, ProColumns } from '@jetlinks/pro-table';
+import { service } from '@/pages/device/Instance/index';
+import { isNoCommunity } from '@/utils/util';
+import { service as categoryService } from '@/pages/device/Category';
+import { service as deptService } from '@/pages/system/Department';
+import { useIntl } from 'umi';
+import { SceneDeviceCard } from '@/components/ProTableCard/CardItems/device';
+import { DeviceModel } from './addModel';
+import { observer } from '@formily/reactive-react';
+
+export default observer(() => {
+  const actionRef = useRef<ActionType>();
+  const intl = useIntl();
+  const [searchParam, setSearchParam] = useState({});
+
+  const columns: ProColumns<DeviceInstance>[] = [
+    {
+      title: 'ID',
+      dataIndex: 'id',
+      width: 300,
+      ellipsis: true,
+      fixed: 'left',
+    },
+    {
+      title: intl.formatMessage({
+        id: 'pages.table.deviceName',
+        defaultMessage: '设备名称',
+      }),
+      dataIndex: 'name',
+      ellipsis: true,
+      width: 200,
+    },
+    {
+      title: intl.formatMessage({
+        id: 'pages.table.productName',
+        defaultMessage: '产品名称',
+      }),
+      dataIndex: 'productId',
+      width: 200,
+      ellipsis: true,
+      valueType: 'select',
+      request: async () => {
+        const res = await service.getProductList();
+        if (res.status === 200) {
+          return res.result.map((pItem: any) => ({ label: pItem.name, value: pItem.id }));
+        }
+        return [];
+      },
+      render: (_, row) => row.productName,
+      filterMultiple: true,
+    },
+    {
+      title: intl.formatMessage({
+        id: 'pages.device.instance.registrationTime',
+        defaultMessage: '注册时间',
+      }),
+      dataIndex: 'registryTime',
+    },
+    {
+      title: intl.formatMessage({
+        id: 'pages.searchTable.titleStatus',
+        defaultMessage: '状态',
+      }),
+      dataIndex: 'state',
+      width: '90px',
+      valueType: 'select',
+      valueEnum: {
+        notActive: {
+          text: intl.formatMessage({
+            id: 'pages.device.instance.status.notActive',
+            defaultMessage: '禁用',
+          }),
+          status: 'notActive',
+        },
+        offline: {
+          text: intl.formatMessage({
+            id: 'pages.device.instance.status.offLine',
+            defaultMessage: '离线',
+          }),
+          status: 'offline',
+        },
+        online: {
+          text: intl.formatMessage({
+            id: 'pages.device.instance.status.onLine',
+            defaultMessage: '在线',
+          }),
+          status: 'online',
+        },
+      },
+      filterMultiple: false,
+    },
+    {
+      dataIndex: 'classifiedId',
+      title: '产品分类',
+      valueType: 'treeSelect',
+      hideInTable: true,
+      fieldProps: {
+        fieldNames: {
+          label: 'name',
+          value: 'id',
+        },
+      },
+      request: () =>
+        categoryService
+          .queryTree({
+            paging: false,
+          })
+          .then((resp: any) => resp.result),
+    },
+    {
+      title: '网关类型',
+      dataIndex: 'accessProvider',
+      width: 150,
+      ellipsis: true,
+      valueType: 'select',
+      hideInTable: true,
+      request: () =>
+        service.getProviders().then((resp: any) => {
+          return (resp?.result || [])
+            .filter((i: any) =>
+              !isNoCommunity
+                ? [
+                    'mqtt-server-gateway',
+                    'http-server-gateway',
+                    'mqtt-client-gateway',
+                    'tcp-server-gateway',
+                  ].includes(i.id)
+                : i,
+            )
+            .map((item: any) => ({
+              label: item.name,
+              value: `accessProvider is ${item.id}`,
+            }));
+        }),
+    },
+    {
+      dataIndex: 'productId$product-info',
+      title: '接入方式',
+      valueType: 'select',
+      hideInTable: true,
+      request: () =>
+        service.queryGatewayList().then((resp: any) =>
+          resp.result.map((item: any) => ({
+            label: item.name,
+            value: `accessId is ${item.id}`,
+          })),
+        ),
+    },
+    {
+      dataIndex: 'deviceType',
+      title: '设备类型',
+      valueType: 'select',
+      hideInTable: true,
+      valueEnum: {
+        device: {
+          text: '直连设备',
+          status: 'device',
+        },
+        childrenDevice: {
+          text: '网关子设备',
+          status: 'childrenDevice',
+        },
+        gateway: {
+          text: '网关设备',
+          status: 'gateway',
+        },
+      },
+    },
+    {
+      dataIndex: 'id$dim-assets',
+      title: '所属组织',
+      valueType: 'treeSelect',
+      hideInTable: true,
+      fieldProps: {
+        fieldNames: {
+          label: 'name',
+          value: 'value',
+        },
+      },
+      request: () =>
+        deptService
+          .queryOrgThree({
+            paging: false,
+          })
+          .then((resp) => {
+            const formatValue = (list: any[]) => {
+              const _list: any[] = [];
+              list.forEach((item) => {
+                if (item.children) {
+                  item.children = formatValue(item.children);
+                }
+                _list.push({
+                  ...item,
+                  value: JSON.stringify({
+                    assetType: 'device',
+                    targets: [
+                      {
+                        type: 'org',
+                        id: item.id,
+                      },
+                    ],
+                  }),
+                });
+              });
+              return _list;
+            };
+            return formatValue(resp.result);
+          }),
+    },
+  ];
+
+  return (
+    <>
+      <SearchComponent
+        field={columns}
+        model={'simple'}
+        enableSave={false}
+        onSearch={async (data) => {
+          actionRef.current?.reset?.();
+          setSearchParam(data);
+        }}
+        target="scene-trugger-device"
+        defaultParam={[
+          {
+            terms: [
+              {
+                column: 'productId',
+                value: DeviceModel.productId,
+              },
+            ],
+          },
+        ]}
+      />
+      <div>
+        <ProTableCard<DeviceInstance>
+          actionRef={actionRef}
+          columns={columns}
+          rowKey="id"
+          search={false}
+          gridColumn={2}
+          columnEmptyText={''}
+          onlyCard={true}
+          tableAlertRender={false}
+          rowSelection={{
+            type: 'radio',
+            selectedRowKeys: [DeviceModel.deviceId],
+            onChange: (_, selectedRows) => {
+              if (selectedRows.length) {
+                const item = selectedRows[0];
+                DeviceModel.deviceId = item.id;
+                DeviceModel.selectorValues = [{ value: DeviceModel.deviceId, name: item.name }];
+              } else {
+                DeviceModel.deviceId = '';
+                DeviceModel.selectorValues = [];
+              }
+            },
+          }}
+          request={(params) =>
+            service.query({
+              ...params,
+              sorts: [{ name: 'createTime', order: 'desc' }],
+            })
+          }
+          params={searchParam}
+          cardRender={(record) => (
+            <SceneDeviceCard showBindBtn={false} showTool={false} {...record} />
+          )}
+          height={'none'}
+        />
+      </div>
+    </>
+  );
+});

+ 90 - 0
src/pages/rule-engine/Scene/Save/device/index.less

@@ -0,0 +1,90 @@
+@import '~antd/es/style/themes/default.less';
+
+.steps-steps {
+  width: 100%;
+  margin-bottom: 17px;
+  padding-bottom: 17px;
+  border-bottom: 1px solid #f0f0f0;
+}
+
+.steps-content {
+  width: 100%;
+}
+
+.trigger-way-warp {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 8px;
+  width: 100%;
+
+  .trigger-way-item {
+    display: flex;
+    justify-content: space-between;
+    width: 237px;
+    //width: 100%;
+    padding: 16px;
+    border: 1px solid #e0e4e8;
+    border-radius: 2px;
+    cursor: pointer;
+    opacity: 0.6;
+    transition: all 0.3s;
+
+    .way-item-title {
+      p {
+        margin-bottom: 8px;
+        font-weight: bold;
+        font-size: 16px;
+      }
+
+      span {
+        color: rgba(#000, 0.35);
+        font-size: 12px;
+      }
+    }
+
+    .way-item-image {
+      margin: 0 !important;
+    }
+
+    &:hover {
+      color: @primary-color-hover;
+      opacity: 0.8;
+    }
+
+    &.active {
+      border-color: @primary-color-active;
+      opacity: 1;
+    }
+  }
+
+  &.disabled {
+    .trigger-way-item {
+      cursor: not-allowed;
+
+      &:hover {
+        color: initial;
+        opacity: 0.6;
+      }
+
+      &.active {
+        opacity: 1;
+      }
+    }
+  }
+}
+
+.device-title {
+  top: 5px;
+  left: 0;
+  width: 760px;
+  height: 38px;
+  padding: 8px 16px 8px 16px;
+  background: rgba(250, 178, 71, 0.1);
+  border: 1px solid rgba(250, 178, 71, 0.4);
+  border-radius: 4px;
+
+  .device-title-icon {
+    margin-right: 5px;
+    color: #fab247;
+  }
+}

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

@@ -1,10 +1,26 @@
 import Terms from '@/pages/rule-engine/Scene/Save/terms';
+import { AddButton } from '@/pages/rule-engine/Scene/Save/components/Buttons';
+import { useState } from 'react';
+import AddModel from './addModel';
 
 export default () => {
+  const [visible, setVisible] = useState(false);
+  const [label] = useState('点击配置设备触发');
+
   return (
     <div>
-      <div></div>
+      <div>
+        <AddButton
+          style={{ width: '100%' }}
+          onClick={() => {
+            setVisible(true);
+          }}
+        >
+          {label}
+        </AddButton>
+      </div>
       <Terms />
+      {visible && <AddModel />}
     </div>
   );
 };

+ 91 - 0
src/pages/rule-engine/Scene/Save/device/org.tsx

@@ -0,0 +1,91 @@
+import ProTable from '@jetlinks/pro-table';
+import SearchComponent from '@/components/SearchComponent';
+import { DeviceModel } from './addModel';
+import { observer } from '@formily/reactive-react';
+import type { DepartmentItem } from '@/pages/system/Department/typings';
+import { service } from '@/pages/system/Department';
+import { useState, useRef } from 'react';
+import type { ActionType, ProColumns } from '@jetlinks/pro-table';
+
+export default observer(() => {
+  const actionRef = useRef<ActionType>();
+  const [sortParam, setSortParam] = useState<any>({ name: 'sortIndex', order: 'asc' });
+  const [param, setParam] = useState({});
+
+  const columns: ProColumns<DepartmentItem>[] = [
+    {
+      title: '名称',
+      dataIndex: 'name',
+    },
+    {
+      title: '排序',
+      dataIndex: 'sortIndex',
+      valueType: 'digit',
+      sorter: true,
+      render: (_, record) => <>{record.sortIndex}</>,
+    },
+  ];
+
+  return (
+    <>
+      <SearchComponent<DepartmentItem>
+        field={columns}
+        enableSave={false}
+        onSearch={(data) => {
+          setParam(data);
+        }}
+        model={'simple'}
+        target="scene-triggrt-device-category"
+      />
+      <ProTable<DepartmentItem>
+        params={param}
+        rowKey="id"
+        search={false}
+        tableClassName={''}
+        columns={columns}
+        pagination={false}
+        actionRef={actionRef}
+        columnEmptyText={''}
+        tableAlertRender={false}
+        rowSelection={{
+          type: 'radio',
+          selectedRowKeys: [DeviceModel.orgId],
+          onChange: (_, selectedRows) => {
+            if (selectedRows.length) {
+              const item = selectedRows[0];
+              DeviceModel.orgId = item.id;
+              DeviceModel.selectorValues = [{ value: DeviceModel.orgId, name: item.name }];
+            } else {
+              DeviceModel.orgId = '';
+              DeviceModel.selectorValues = [];
+            }
+          },
+        }}
+        onChange={(_, f, sorter: any) => {
+          if (sorter.order) {
+            setSortParam({ name: sorter.columnKey, order: sorter.order.replace('end', '') });
+          } else {
+            setSortParam({ name: 'sortIndex', value: 'asc' });
+          }
+        }}
+        request={async (params) => {
+          const response = await service.queryOrgThree({
+            paging: false,
+            sorts: [sortParam],
+            ...params,
+          });
+          return {
+            code: response.message,
+            result: {
+              data: response.result as DepartmentItem[],
+              pageIndex: 0,
+              pageSize: 0,
+              total: 0,
+            },
+            status: response.status,
+          };
+        }}
+      />
+    </>
+  );
+});

+ 243 - 0
src/pages/rule-engine/Scene/Save/device/product.tsx

@@ -0,0 +1,243 @@
+import { ProTableCard } from '@/components';
+import SearchComponent from '@/components/SearchComponent';
+import type { ProductItem } from '@/pages/device/Product/typings';
+import { useRef, useState } from 'react';
+import type { ActionType, ProColumns } from '@jetlinks/pro-table';
+import { service } from '@/pages/device/Product/index';
+import { SceneProductCard } from '@/components/ProTableCard/CardItems/product';
+import { isNoCommunity } from '@/utils/util';
+import { useIntl } from 'umi';
+import { service as categoryService } from '@/pages/device/Category';
+import { service as deptService } from '@/pages/system/Department';
+import { DeviceModel } from './addModel';
+import { observer } from '@formily/reactive-react';
+
+export default observer(() => {
+  const actionRef = useRef<ActionType>();
+  const intl = useIntl();
+  const [searchParam, setSearchParam] = useState({});
+
+  const columns: ProColumns<ProductItem>[] = [
+    {
+      title: 'ID',
+      dataIndex: 'id',
+      width: 300,
+      ellipsis: true,
+      fixed: 'left',
+    },
+    {
+      title: '名称',
+      dataIndex: 'name',
+      width: 200,
+      ellipsis: true,
+    },
+    {
+      title: '网关类型',
+      dataIndex: 'accessProvider',
+      width: 150,
+      ellipsis: true,
+      valueType: 'select',
+      hideInTable: true,
+      request: () =>
+        service.getProviders().then((resp: any) => {
+          if (isNoCommunity) {
+            return (resp?.result || []).map((item: any) => ({
+              label: item.name,
+              value: item.id,
+            }));
+          } else {
+            return (resp?.result || [])
+              .filter((i: any) =>
+                [
+                  'mqtt-server-gateway',
+                  'http-server-gateway',
+                  'mqtt-client-gateway',
+                  'tcp-server-gateway',
+                ].includes(i.id),
+              )
+              .map((item: any) => ({
+                label: item.name,
+                value: item.id,
+              }));
+          }
+        }),
+    },
+    {
+      title: '接入方式',
+      dataIndex: 'accessName',
+      width: 150,
+      ellipsis: true,
+      valueType: 'select',
+      request: () =>
+        service.queryGatewayList().then((resp: any) =>
+          resp.result.map((item: any) => ({
+            label: item.name,
+            value: item.name,
+          })),
+        ),
+    },
+    {
+      title: '设备类型',
+      dataIndex: 'deviceType',
+      valueType: 'select',
+      valueEnum: {
+        device: {
+          text: '直连设备',
+          status: 'device',
+        },
+        childrenDevice: {
+          text: '网关子设备',
+          status: 'childrenDevice',
+        },
+        gateway: {
+          text: '网关设备',
+          status: 'gateway',
+        },
+      },
+      width: 150,
+      render: (_, row) => <>{row.deviceType ? row.deviceType.text : undefined}</>,
+    },
+    {
+      title: '状态',
+      dataIndex: 'state',
+      valueType: 'select',
+      width: '90px',
+      valueEnum: {
+        0: {
+          text: intl.formatMessage({
+            id: 'pages.device.product.status.disabled',
+            defaultMessage: '禁用',
+          }),
+          status: 0,
+        },
+        1: {
+          text: intl.formatMessage({
+            id: 'pages.device.product.status.enabled',
+            defaultMessage: '正常',
+          }),
+          status: 1,
+        },
+      },
+    },
+    {
+      dataIndex: 'describe',
+      title: intl.formatMessage({
+        id: 'pages.system.description',
+        defaultMessage: '说明',
+      }),
+      ellipsis: true,
+      width: 300,
+      // hideInSearch: true,
+    },
+    {
+      dataIndex: 'classifiedId',
+      title: '分类',
+      valueType: 'treeSelect',
+      hideInTable: true,
+      fieldProps: {
+        fieldNames: {
+          label: 'name',
+          value: 'id',
+        },
+      },
+      request: () =>
+        categoryService
+          .queryTree({
+            paging: false,
+          })
+          .then((resp: any) => resp.result),
+    },
+    {
+      dataIndex: 'id$dim-assets',
+      title: '所属组织',
+      valueType: 'treeSelect',
+      hideInTable: true,
+      fieldProps: {
+        fieldNames: {
+          label: 'name',
+          value: 'value',
+        },
+      },
+      request: () =>
+        deptService
+          .queryOrgThree({
+            paging: false,
+          })
+          .then((resp) => {
+            const formatValue = (list: any[]) => {
+              const _list: any[] = [];
+              list.forEach((item) => {
+                if (item.children) {
+                  item.children = formatValue(item.children);
+                }
+                _list.push({
+                  ...item,
+                  value: JSON.stringify({
+                    assetType: 'product',
+                    targets: [
+                      {
+                        type: 'org',
+                        id: item.id,
+                      },
+                    ],
+                  }),
+                });
+              });
+              return _list;
+            };
+            return formatValue(resp.result);
+          }),
+    },
+  ];
+  return (
+    <div>
+      <SearchComponent
+        field={columns}
+        model={'simple'}
+        enableSave={false}
+        onSearch={async (data) => {
+          actionRef.current?.reset?.();
+          setSearchParam(data);
+        }}
+        target="department-assets-product"
+      />
+      <div
+        style={{
+          height: 'calc(100vh - 440px)',
+          overflowY: 'auto',
+        }}
+      >
+        <ProTableCard<ProductItem>
+          actionRef={actionRef}
+          columns={columns}
+          rowKey="id"
+          search={false}
+          gridColumn={2}
+          columnEmptyText={''}
+          onlyCard={true}
+          tableAlertRender={false}
+          rowSelection={{
+            type: 'radio',
+            selectedRowKeys: [DeviceModel.productId],
+            onChange: (_, selectedRows) => {
+              // console.log(selectedRowKeys,selectedRows)
+              DeviceModel.productId = selectedRows.map((item) => item.id)[0];
+              DeviceModel.productDetail = selectedRows?.[0];
+            },
+          }}
+          request={(params) =>
+            service.query({
+              ...params,
+              sorts: [{ name: 'createTime', order: 'desc' }],
+            })
+          }
+          params={searchParam}
+          cardRender={(record) => (
+            <SceneProductCard showBindBtn={false} showTool={false} {...record} />
+          )}
+          height={'none'}
+        />
+      </div>
+    </div>
+  );
+});

+ 37 - 0
src/pages/rule-engine/Scene/Save/device/type.tsx

@@ -0,0 +1,37 @@
+import { Form } from 'antd';
+import TopCard from './TopCard';
+
+const TypeList = [
+  {
+    label: '自定义',
+    value: 'custom',
+    image: require('/public/images/scene/device-custom.png'),
+    tip: '自定义选择当前产品下的任意设备',
+  },
+  {
+    label: '全部',
+    value: 'all',
+    image: require('/public/images/scene/trigger-device-all.png'),
+    tip: '产品下的所有设备',
+  },
+  {
+    label: '按组织',
+    value: 'org',
+    image: require('/public/images/scene/trigger-device-org.png'),
+    tip: '选择产品下归属于具体组织的设备',
+  },
+];
+
+export default () => {
+  const [form] = Form.useForm();
+
+  return (
+    <div>
+      <Form form={form} layout={'vertical'}>
+        <Form.Item name="type" label="触发类型" required>
+          <TopCard typeList={TypeList} />
+        </Form.Item>
+      </Form>
+    </div>
+  );
+};