Переглянути джерело

feat(场景联动): 完善新增

xieyonghong 3 роки тому
батько
коміт
00e32dbe57

+ 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,
       }}
     >

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

@@ -118,7 +118,7 @@ const ActionItem = (props: ActionProps) => {
   );
 
   useEffect(() => {
-    if (type1 === 'message') {
+    if (type1 === 'notify') {
       queryMessageTypes();
     }
   }, [type1]);
@@ -135,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' },
             ]}
@@ -146,7 +146,7 @@ const ActionItem = (props: ActionProps) => {
             }}
           />
         </Form.Item>
-        {type1 === 'message' && MessageNodes}
+        {type1 === 'notify' && MessageNodes}
         {type1 === 'device' && (
           <DeviceSelect
             name={props.name}
@@ -164,7 +164,7 @@ const ActionItem = (props: ActionProps) => {
           </Form.Item>
         )}
       </div>
-      {type1 === 'message' && templateData ? (
+      {type1 === 'notify' && templateData ? (
         <MessageContent
           form={props.form}
           template={templateData}

+ 9 - 2
src/pages/rule-engine/Scene/Save/action/device/functionCall.tsx

@@ -78,9 +78,16 @@ export default (props: FunctionCallProps) => {
       case 'double':
         return <InputNumber style={{ width: '100%' }} placeholder={'请输入' + name} />;
       case 'date':
-        // @ts-ignore
         return (
-          <DatePicker format={record.format || 'YYYY-MM-DD HH:mm:ss'} style={{ width: '100%' }} />
+          <>
+            {
+              // @ts-ignore
+              <DatePicker
+                format={record.format || 'YYYY-MM-DD HH:mm:ss'}
+                style={{ width: '100%' }}
+              />
+            }
+          </>
         );
       default:
         return <Input placeholder={'请输入' + name} />;

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

@@ -8,20 +8,20 @@ interface InputNumberProps {
 
 export default (props: InputNumberProps) => {
   const [time, setTime] = useState(props.value?.time || 0);
-  const [unit, setUnit] = useState(props.value?.unit || 'second');
+  const [unit, setUnit] = useState(props.value?.unit || 'seconds');
 
   useEffect(() => {
     setTime(props.value?.time || 0);
-    setUnit(props.value?.unit || 'second');
+    setUnit(props.value?.unit || 'seconds');
   }, [props.value]);
 
   const TimeTypeAfter = (
     <Select
       value={unit}
       options={[
-        { label: '秒', value: 'second' },
-        { label: '分', value: 'minute' },
-        { label: '小时', value: 'hour' },
+        { label: '秒', value: 'seconds' },
+        { label: '分', value: 'minutes' },
+        { label: '小时', value: 'hours' },
       ]}
       onChange={(key) => {
         if (props.onChange) {

+ 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}
           />

+ 197 - 51
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();
@@ -15,6 +37,12 @@ export default () => {
 
   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 getDetail = async () => {
     // TODO 回显数据
@@ -30,13 +58,27 @@ 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',
@@ -84,16 +126,71 @@ export default () => {
     },
   };
 
+  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>
+  );
+
+  console.log(shakeLimit);
   return (
     <PageContainer>
       <Card>
-        <Form
-          form={form}
-          colon={false}
-          layout={'vertical'}
-          preserve={false}
-          onValuesChange={() => {}}
-        >
+        <Form form={form} colon={false} layout={'vertical'} preserve={false}>
           <Form.Item name={'name'} label={'名称'}>
             <Input placeholder={'请输入名称'} />
           </Form.Item>
@@ -101,16 +198,56 @@ export default () => {
             <Form.Item name={['trigger', 'type']}>
               <TriggerWay onSelect={setTriggerType} />
             </Form.Item>
+            {triggerType === TriggerWayType.timing && (
+              <Form.Item name={['trigger', 'timer']}>
+                <TimingTrigger />
+              </Form.Item>
+            )}
+            {triggerType === TriggerWayType.device && <TriggerDevice form={form} />}
           </Form.Item>
-          <Form.Item noStyle>
-            <TriggerTerm
-              ref={triggerRef}
-              params={requestParams}
-              value={triggerValue}
-              onChange={console.log}
-            />
+          {triggerType === TriggerWayType.device && (
+            <Form.Item label={AntiShake}>
+              <TriggerTerm
+                ref={triggerRef}
+                params={requestParams}
+                // value={triggerValue}
+              />
+            </Form.Item>
+          )}
+          <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 }) => (
                 <>
@@ -133,40 +270,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>
+        {/*<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>
   );

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

@@ -1,10 +1,262 @@
-import { Form } from 'antd';
-import TriggerWay from '../components/TriggerWay';
+import { useCallback, useEffect, useState } from 'react';
+import { Col, Form, Row, Select, Space, TreeSelect } from 'antd';
+import type { FormInstance } 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.status === 200) {
+        setSelectorOptions(resp.result);
+      }
+    });
+  };
+
+  const getOrgTree = useCallback(() => {
+    queryOrgTree(productId).then((resp) => {
+      if (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>
+            <Form.Item name={['trigger', 'device', 'selectorValues']}>
+              {selector === 'fixed' && <Device productId={productId} />}
+              {selector === 'org' && (
+                <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];
 

+ 26 - 17
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,7 +61,7 @@ const Scene = () => {
         popConfirm={{
           title: intl.formatMessage({
             id: `pages.data.option.${
-              record.state.value !== 'notActive' ? 'disabled' : 'enabled'
+              record.state.value !== 'started' ? 'disabled' : 'enabled'
             }.tips`,
             defaultMessage: '确认禁用?',
           }),
@@ -70,21 +76,23 @@ const Scene = () => {
           },
         }}
         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 +103,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',