xieyonghong 3 лет назад
Родитель
Сommit
194860b8cb

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

@@ -0,0 +1,68 @@
+@bgColor: #fafafa;
+
+.scene-save {
+  .trigger-type-content,
+  & .scene-actions {
+    .actions-item {
+      padding: 24px 24px 0 24px;
+      background-color: @bgColor;
+
+      &:not(:first-child) {
+        margin-top: 16px;
+      }
+
+      .actions-item-title {
+        margin-bottom: 16px;
+      }
+
+      .actions-item-footer {
+        display: flex;
+        justify-content: flex-end;
+        padding-top: 16px;
+        border-top: 1px solid #2f54eb;
+      }
+
+      .template-variable {
+        margin-bottom: 16px;
+        padding: 16px;
+        border: 1px solid #e6e6e6;
+      }
+    }
+  }
+
+  .ant-form-item-with-help .ant-form-item-explain {
+    height: 0 !important;
+  }
+
+  //.trigger-type-content {
+  //  > .ant-row {
+  //    margin-bottom: 24px;
+  //
+  //    &:last-child,
+  //    &:first-child {
+  //      margin-bottom: 0;
+  //    }
+  //  }
+  //}
+}
+
+.scene-content {
+  position: relative;
+
+  .scene-content-left {
+    width: 66.66%;
+  }
+
+  .scene-content-right {
+    position: absolute;
+    top: 0;
+    right: 0;
+    width: 33.33%;
+    height: 100%;
+
+    > div {
+      height: 100%;
+      overflow-y: auto;
+    }
+  }
+}

+ 432 - 44
src/pages/rule-engine/Scene/Save/index.tsx

@@ -1,57 +1,445 @@
 import { PageContainer } from '@ant-design/pro-layout';
-import { Button, Card, Form, Input } from 'antd';
-import useLocation from '@/hooks/route/useLocation';
-import Device from '../Save/device/index';
-import Manual from '../Save/manual/index';
-import Timer from '../Save/timer/index';
-import { TitleComponent } from '@/components';
+import { Button, Card, Form, Input, InputNumber, Radio, Space, Switch, Tooltip } from 'antd';
+import { useIntl, useHistory } from 'umi';
+import { useCallback, useEffect, useRef, useState } from 'react';
+import { PermissionButton, TitleComponent } from '@/components';
+import ActionItems from './action/action';
+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';
+import './index.less';
+import { model } from '@formily/reactive';
+import type { FormModelType } from '@/pages/rule-engine/Scene/typings';
+import { onlyMessage } from '@/utils/util';
+import Explanation from './Explanation';
+import { getMenuPathByCode } from '@/utils/menu';
+import { useLocation } from '@/hooks';
+
+type ShakeLimitType = {
+  enabled: boolean;
+  groupType?: string;
+  time?: number;
+  threshold?: number;
+  alarmFirst?: boolean;
+};
+
+const DefaultShakeLimit = {
+  enabled: false,
+  alarmFirst: false,
+};
+
+export let FormModel = model<FormModelType>({});
 
 export default () => {
   const location = useLocation();
-  const triggerType = location?.query?.triggerType || '';
-
-  const triggerRender = (type: string) => {
-    switch (type) {
-      case 'device':
-        return (
-          <Form.Item label={<TitleComponent style={{ fontSize: 14 }} data={'设备触发'} />}>
-            <Device />
-          </Form.Item>
-        );
-      case 'manual':
-        return (
-          <Form.Item label={<TitleComponent style={{ fontSize: 14 }} data={'手动触发'} />}>
-            <Manual />
-          </Form.Item>
-        );
-      case 'timer':
-        return (
-          <Form.Item label={<TitleComponent style={{ fontSize: 14 }} data={'定时触发'} />}>
-            <Timer />
-          </Form.Item>
-        );
-      default:
-        return null;
+  const [form] = Form.useForm();
+  const intl = useIntl();
+  const triggerRef = useRef<any>();
+  const history = useHistory();
+
+  const { getOtherPermission } = PermissionButton.usePermission('rule-engine/Scene');
+  const [triggerType, setTriggerType] = useState('device');
+
+  const [loading, setLoading] = useState(false);
+  const [parallel, setParallel] = useState(true); // 是否并行
+  const [shakeLimit, setShakeLimit] = useState<ShakeLimitType>(DefaultShakeLimit);
+  const [targetTypeDisabled, setTargetTypeDisabled] = useState(false);
+
+  const [requestParams, setRequestParams] = useState<any>(undefined);
+  const [triggerValue, setTriggerValue] = useState<any>([]);
+  const [triggerDatas, setTriggerDatas] = useState<any>({});
+  const [actionParams, setActionParams] = useState<any>(undefined);
+  const [actionDataCount, setActionDataCount] = useState(0);
+
+  const [actionsData, setActionsData] = useState<any[]>([]);
+  const [isEdit, setIsEdit] = useState(false);
+  const [view, setView] = useState<boolean>(false);
+
+  useEffect(() => {
+    FormModel = {};
+  }, []);
+
+  const getDetail = useCallback(
+    async (id: string) => {
+      const resp = await service.detail(id);
+      if (resp.status === 200 && resp.result) {
+        setIsEdit(true);
+        const _data: any = resp.result;
+        FormModel = _data;
+        console.log(_data);
+
+        form.setFieldsValue(_data);
+        // debugger;
+        setParallel(_data.parallel);
+
+        setTriggerValue({ trigger: _data.terms || [] });
+        setTriggerDatas(_data.trigger);
+        setActionParams({ trigger: _data.trigger });
+        if (_data.trigger?.shakeLimit) {
+          setShakeLimit(_data.trigger?.shakeLimit || DefaultShakeLimit);
+        }
+        if (_data.trigger?.device) {
+          setRequestParams({ trigger: _data.trigger });
+        }
+
+        if (_data.actions) {
+          setActionsData(_data.actions);
+        }
+      }
+    },
+    [triggerRef],
+  );
+
+  useEffect(() => {
+    const params = new URLSearchParams(location.search);
+    const id = params.get('id');
+    const targetType = params.get('targetType');
+    if (id) {
+      getDetail(id);
+    }
+
+    if (targetType) {
+      setTargetTypeDisabled(true);
+    }
+
+    if (location && location.state) {
+      setView(location.state.view);
+    }
+  }, [location]);
+
+  const saveData = useCallback(async () => {
+    const formData = await form.validateFields();
+    let triggerData = undefined;
+    // 获取触发条件数据
+    if (triggerRef.current && formData.trigger) {
+      triggerData = await triggerRef.current.getTriggerForm();
+      if (triggerData) {
+        formData.terms = triggerData.trigger;
+      }
     }
+    console.log('save', formData);
+    if (formData) {
+      if (shakeLimit.enabled) {
+        formData.trigger = {
+          ...formData.trigger,
+          shakeLimit: shakeLimit,
+        };
+      }
+      setLoading(true);
+      const resp = formData.id ? await service.updateScene(formData) : await service.save(formData);
+
+      // 处理跳转新增
+      if ((window as any).onTabSaveSuccess) {
+        if (resp.result) {
+          (window as any).onTabSaveSuccess(resp);
+          setTimeout(() => window.close(), 300);
+        }
+      }
+
+      setLoading(false);
+      if (resp.status === 200) {
+        onlyMessage('操作成功');
+        const url = getMenuPathByCode('rule-engine/Scene');
+        history.push(url);
+      } else {
+        onlyMessage(resp.message);
+      }
+    }
+  }, [shakeLimit]);
+
+  const AntiShake = (
+    <Space>
+      <TitleComponent data={'触发条件'} style={{ margin: 0 }} />
+      <Switch
+        checked={shakeLimit.enabled}
+        checkedChildren="开启防抖"
+        unCheckedChildren="关闭防抖"
+        onChange={(e) => {
+          const newShake = {
+            ...shakeLimit,
+            enabled: e,
+          };
+          setShakeLimit(newShake);
+        }}
+        style={{ marginRight: 16 }}
+      />
+      {shakeLimit.enabled && (
+        <>
+          <InputNumber
+            value={shakeLimit.time}
+            min={1}
+            max={100}
+            onChange={(e: number) => {
+              const newShake = {
+                ...shakeLimit,
+                time: e,
+              };
+              setShakeLimit(newShake);
+            }}
+          />
+          <span style={{ padding: '0 16px' }}> 秒内发生 </span>
+          <InputNumber
+            value={shakeLimit.threshold}
+            min={1}
+            max={100}
+            onChange={(e: number) => {
+              const newShake = {
+                ...shakeLimit,
+                threshold: e,
+              };
+              setShakeLimit(newShake);
+            }}
+          />
+          <span style={{ padding: '0 16px' }}>次及以上时,处理</span>
+          <Radio.Group
+            value={shakeLimit.alarmFirst}
+            options={[
+              { label: '第一次', value: true },
+              { label: '最后一次', value: false },
+            ]}
+            optionType="button"
+            onChange={(e) => {
+              const newShake = {
+                ...shakeLimit,
+                alarmFirst: e.target.value,
+              };
+              setShakeLimit(newShake);
+            }}
+          ></Radio.Group>
+        </>
+      )}
+    </Space>
+  );
+
+  const hasKeyInObject = (keys: string[], obj: any) => {
+    return keys.some((key) => key in obj);
   };
 
   return (
     <PageContainer>
       <Card>
-        <Form name="timer" layout={'vertical'}>
-          {triggerRender(triggerType)}
-          <Form.Item
-            label={<TitleComponent style={{ fontSize: 14 }} data={'说明'} />}
-            name="description"
-          >
-            <Input.TextArea showCount maxLength={200} placeholder={'请输入说明'} rows={4} />
-          </Form.Item>
-          <Form.Item>
-            <Button type="primary" htmlType="submit">
-              保存
-            </Button>
-          </Form.Item>
-        </Form>
+        <div className={'scene-content'}>
+          <div className={'scene-content-left'}>
+            <Form
+              scrollToFirstError={{
+                behavior: 'smooth',
+                block: 'end',
+              }}
+              form={form}
+              colon={false}
+              name="basicForm"
+              layout={'vertical'}
+              preserve={false}
+              className={'scene-save'}
+              onValuesChange={(changeValue, allValues) => {
+                if (changeValue.trigger) {
+                  if (changeValue.trigger.device) {
+                    if (
+                      changeValue.trigger.device.productId ||
+                      changeValue.trigger.device.selectorValues ||
+                      (changeValue.trigger.device.operation &&
+                        hasKeyInObject(
+                          ['operator', 'eventId', 'functionId'],
+                          changeValue.trigger.device.operation,
+                        ))
+                    ) {
+                      setTriggerValue([]);
+                      setRequestParams({ trigger: allValues.trigger });
+                      setTriggerDatas(allValues.trigger);
+                    }
+
+                    if (
+                      hasKeyInObject(['productId'], changeValue.trigger.device) ||
+                      (changeValue.trigger.device.operation &&
+                        hasKeyInObject(
+                          ['operator', 'eventId', 'functionId'],
+                          changeValue.trigger.device.operation,
+                        ))
+                    ) {
+                      setActionParams({ trigger: allValues.trigger }); // 用于内置参数请求
+                    }
+                  } else if (['timer', 'manual'].includes(changeValue.trigger.type)) {
+                    setActionParams({ trigger: allValues.trigger }); // 用于内置参数请求
+                  }
+                }
+                if (allValues.actions) {
+                  // debugger;
+                  console.log(allValues.actions, 'onValuesChange');
+                  setActionsData(allValues.actions);
+                }
+                FormModel = { ...allValues };
+              }}
+            >
+              <Form.Item
+                name={'name'}
+                label={<TitleComponent data={'名称'} style={{ margin: 0 }} />}
+                rules={[
+                  { required: true, message: '请输入名称' },
+                  {
+                    max: 64,
+                    message: intl.formatMessage({
+                      id: 'pages.form.tip.max64',
+                      defaultMessage: '最多输入64个字符',
+                    }),
+                  },
+                ]}
+              >
+                <Input placeholder={'请输入名称'} />
+              </Form.Item>
+              <Form.Item
+                label={<TitleComponent data={'触发方式'} style={{ margin: 0 }} />}
+                required
+              >
+                <Form.Item
+                  name={['trigger', 'type']}
+                  rules={[{ required: true, message: '请选择触发方式' }]}
+                  initialValue={'device'}
+                >
+                  <TriggerWay onSelect={setTriggerType} disabled={isEdit || targetTypeDisabled} />
+                </Form.Item>
+                {triggerType === TriggerWayType.timing && (
+                  <TimingTrigger
+                    name={['trigger']}
+                    form={form}
+                    className={'trigger-type-content'}
+                  />
+                )}
+                {triggerType === TriggerWayType.device && (
+                  <TriggerDevice
+                    value={triggerDatas}
+                    className={'trigger-type-content'}
+                    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={true}>
+                <Input />
+              </Form.Item>
+              <Form.Item
+                label={
+                  <Space>
+                    <TitleComponent data={<span>执行动作</span>} style={{ margin: 0 }} />
+                    <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>
+                  </Space>
+                }
+              >
+                <Form.List name="actions">
+                  {(fields, { add, remove, move }, { errors }) => (
+                    <>
+                      <div className={'scene-actions'} style={{ paddingBottom: 24 }}>
+                        {fields.map(({ key, name, ...restField }) => (
+                          <ActionItems
+                            key={key}
+                            form={form}
+                            restField={restField}
+                            name={name}
+                            trigger={actionParams}
+                            triggerType={triggerType}
+                            triggerRef={triggerRef.current}
+                            onRemove={() => {
+                              remove(name);
+                              setActionDataCount(actionDataCount - 1);
+                            }}
+                            onMove={(type) => {
+                              if (type === 'up') {
+                                move(name, name - 1);
+                              } else {
+                                move(name, name + 1);
+                              }
+                            }}
+                            actionItemData={actionsData[name]}
+                            isLast={!actionDataCount || actionDataCount - 1 === name}
+                            parallel={parallel}
+                            isEdit={isEdit}
+                          />
+                        ))}
+                        <Form.Item noStyle>
+                          <Button
+                            type="primary"
+                            ghost
+                            style={{
+                              width: '100%',
+                              marginTop: 16,
+                            }}
+                            onClick={() => {
+                              add();
+                              setActionDataCount(actionDataCount + 1);
+                            }}
+                            icon={<PlusOutlined />}
+                          >
+                            新增
+                          </Button>
+                        </Form.Item>
+                      </div>
+                      <Form.ErrorList errors={errors} />
+                    </>
+                  )}
+                </Form.List>
+              </Form.Item>
+              <Form.Item
+                label={<TitleComponent data={'说明'} style={{ margin: 0 }} />}
+                name={'description'}
+              >
+                <Input.TextArea showCount maxLength={200} placeholder={'请输入说明'} rows={4} />
+              </Form.Item>
+              {/*{triggerType === TriggerWayType.device &&*/}
+              {/*requestParams &&*/}
+              {/*requestParams.trigger?.device?.productId ? (*/}
+              {/*  <Form.Item hidden name={['trigger','shakeLimit']}>*/}
+              {/*    <Input />*/}
+              {/*  </Form.Item>*/}
+              {/*) : null}*/}
+              <Form.Item hidden name={'id'}>
+                <Input />
+              </Form.Item>
+              {!view && (
+                <PermissionButton
+                  isPermission={getOtherPermission(['add', 'update'])}
+                  onClick={saveData}
+                  type={'primary'}
+                  loading={loading}
+                  htmlType="submit"
+                >
+                  保存
+                </PermissionButton>
+              )}
+            </Form>
+          </div>
+          <div className={'scene-content-right'}>
+            <Explanation type={triggerType} />
+          </div>
+        </div>
       </Card>
     </PageContainer>
   );

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

@@ -36,7 +36,7 @@ const Scene = () => {
   const { permission } = PermissionButton.usePermission('rule-engine/Scene');
   const [searchParams, setSearchParams] = useState<any>({});
   const [visible, setVisible] = useState<boolean>(false);
-  const [current, setCurrent] = useState<Partial<SceneItem>>({});
+  const [current] = useState<Partial<SceneItem>>({});
   const history = useHistory();
 
   const deleteById = async (id: string) => {
@@ -98,8 +98,8 @@ const Scene = () => {
             : undefined
         }
         onClick={() => {
-          setVisible(true);
-          setCurrent(record);
+          const url = getMenuPathByCode('rule-engine/Scene/Save');
+          history.push(`${url}?id=${record.id}`);
         }}
       >
         <EditOutlined />
@@ -342,10 +342,10 @@ const Scene = () => {
             type="primary"
             isPermission={permission.add}
             onClick={() => {
-              // const url = getMenuPathByCode('rule-engine/Scene/Save');
-              // history.push(url);
-              setCurrent({});
-              setVisible(true);
+              const url = getMenuPathByCode('rule-engine/Scene/Save');
+              history.push(url);
+              // setCurrent({});
+              // setVisible(true);
             }}
           >
             {intl.formatMessage({
@@ -367,8 +367,8 @@ const Scene = () => {
                   title: '查看',
                 }}
                 onClick={() => {
-                  // const url = getMenuPathByCode('rule-engine/Scene/Save');
-                  // history.push(`${url}?id=${record.id}`, { view: true });
+                  const url = getMenuPathByCode('rule-engine/Scene/Save');
+                  history.push(`${url}?id=${record.id}`, { view: true });
                   // setCurrent({})
                   // setVisible(true)
                 }}