wzyyy 3 лет назад
Родитель
Сommit
9ee8ef719c

+ 31 - 4
src/components/ProTableCard/CardItems/Scene/index.less

@@ -57,6 +57,7 @@
           width: 100%;
           .trigger-contents {
             color: rgba(0, 0, 0, 0.85);
+            font-weight: bold;
           }
           .right-item-left {
             width: 15%;
@@ -74,17 +75,43 @@
               width: 100%;
               .trigger-ways {
                 width: 70px;
+                margin-right: 18px;
                 color: rgba(0, 0, 0, 0.85);
                 font-size: 16px;
               }
               .right-item-right-item-contents {
                 display: flex;
-                width: calc(100% - 95px);
+                align-items: center;
+                width: calc(100% - 88px);
                 overflow: hidden;
-                .right-item-right-item-contents-item {
+                .right-item-right-item-contents-text {
                   display: flex;
-                  align-items: center;
-                  color: rgba(0, 0, 0, 0.65);
+                  .right-item-right-item-contents-item {
+                    display: flex;
+                    align-items: center;
+                    width: max-content;
+                    overflow: hidden;
+                    white-space: nowrap;
+                    text-overflow: ellipsis;
+                    .right-item-right-item-contents-item-action {
+                      color: rgba(0, 0, 0, 0.85);
+                      font-weight: bold;
+                    }
+                    .right-item-right-item-contents-item-filter {
+                      color: rgba(0, 0, 0, 0.65);
+                    }
+                    .item-ellipsis {
+                      width: max-content;
+                      margin-right: 8px;
+                      overflow: hidden;
+                      white-space: nowrap;
+                      text-overflow: ellipsis;
+                    }
+                  }
+                }
+                .right-item-right-item-contents-extra {
+                  min-width: 100px;
+                  color: #fab247;
                 }
               }
             }

+ 105 - 231
src/components/ProTableCard/CardItems/Scene/index.tsx

@@ -17,7 +17,7 @@ imageMap.set('timer', require('/public/images/scene/scene-timer.png'));
 imageMap.set('manual', require('/public/images/scene/scene-hand.png'));
 imageMap.set('device', require('/public/images/scene/scene-device.png'));
 
-const iconMap = new Map();
+export const iconMap = new Map();
 iconMap.set('timer', require('/public/images/scene/trigger-type-icon/timing.png'));
 iconMap.set('manual', require('/public/images/scene/trigger-type-icon/manual.png'));
 iconMap.set('device', require('/public/images/scene/trigger-type-icon/device.png'));
@@ -35,7 +35,7 @@ export interface SceneCardProps extends SceneItem {
   onClick?: () => void;
 }
 
-enum TriggerWayType {
+export enum TriggerWayType {
   manual = '手动触发',
   timer = '定时触发',
   device = '设备触发',
@@ -47,145 +47,33 @@ enum UnitEnum {
   hours = '小时',
 }
 
-// const selectorRender = (obj: any) => {
-//   switch (obj?.selector) {
-//     case 'all':
-//       return (
-//         <span>
-//           所有的<span className={styles['trigger-device']}>{obj?.productId}</span>
-//         </span>
-//       );
-//     case 'fixed':
-//       return (
-//         <span>
-//           设备
-//           <span className={styles['trigger-device']}>
-//             {(obj?.selectorValues || '').map((item: any) => item?.name).join(',')}
-//           </span>
-//         </span>
-//       );
-//     case 'org':
-//       return (
-//         <span>
-//           部门
-//           <span className={styles['trigger-device']}>
-//             {(obj?.selectorValues || '').map((item: any) => item?.name).join(',')}
-//           </span>
-//         </span>
-//       );
-//     default:
-//       return '';
-//   }
-// };
-
-// const selectorContextRender = (obj: any) => {
-//   switch (obj?.selector) {
-//     case 'all':
-//       return `所有的${obj?.productId}`;
-//     case 'fixed':
-//       return `设备${(obj?.selectorValues || '').map((item: any) => item?.name).join(',')}`;
-//     case 'org':
-//       return `部门${(obj?.selectorValues || '').map((item: any) => item?.name).join(',')}`;
-//     default:
-//       return '';
-//   }
-// };
-
-// const timerRender = (timer: any) => {
-//   if (timer?.trigger && timer?.mod) {
-//     const trigger = timer?.trigger;
-//     const mod = timer?.mod;
-//     const str: string = trigger === 'week' ? '星期' : trigger === 'month' ? '月' : timer?.cron;
-//     if (mod === 'once') {
-//       return `,每${str}${timer.when.join('/')},${timer?.once.time}执行一次`;
-//     } else {
-//       return `每${str}${timer.when.join('/')},${timer?.period?.from}-${timer?.period.to},每${
-//         timer?.period.every
-//       }${UnitEnum[timer?.period?.unit]}执行一次`;
-//     }
-//   }
-//   return '';
-// };
-
-// const operatorRender = (operation: any) => {
-//   switch (operation?.operator) {
-//     case 'online':
-//       return '上线';
-//     case 'offline':
-//       return '离线';
-//     case 'reportEvent':
-//       return `上报事件${operation?.options?.eventName}`;
-//     case 'reportProperty':
-//       return `上报属性${(operation?.options?.propertiesName || []).join(',')}`;
-//     case 'readProperty':
-//       return `读取属性${(operation?.options?.propertiesName || []).join(',')}`;
-//     case 'writeProperty':
-//       return `修改属性${(operation?.options?.propertiesName || []).join(',')}`;
-//     case 'invokeFunction':
-//       return `调用功能${operation?.options?.functionName}`;
-//     default:
-//       return '';
-//   }
-// };
-
 const notifyRender = (data: ActionsType | undefined) => {
   switch (data?.notify?.notifyType) {
     case 'dingTalk':
       if (data?.options?.provider === 'dingTalkRobotWebHook') {
-        return (
-          <div className={styles['notify-img-highlight']}>
-            通过<span className={'notify-text-highlight'}>群机器人消息</span>
-            发送<span>{data?.options?.templateName || data?.notify?.templateId}</span>
-          </div>
-        );
+        return `通过群机器人消息发送${data?.options?.templateName || data?.notify?.templateId}`;
       }
-      return (
-        <div className={styles['notify-img-highlight']}>
-          向<span>{data?.options?.notifierName || data?.notify?.notifierId}</span>
-          通过<span>钉钉</span>发送
-          <span>{data?.options?.templateName || data?.notify?.templateId}</span>
-        </div>
-      );
+      return `向${data?.options?.notifierName || data?.notify?.notifierId}通过钉钉发送${
+        data?.options?.templateName || data?.notify?.templateId
+      }`;
     case 'weixin':
-      return (
-        <div className={styles['notify-img-highlight']}>
-          向<span>{data?.options?.sendTo || ''}</span>
-          <span>{data?.options?.orgName || ''}</span>
-          <span>{data?.options?.tagName || ''}</span>
-          通过<span>微信</span>发送
-          <span>{data?.options?.templateName || data?.notify?.templateId}</span>
-        </div>
-      );
+      return `向${data?.options?.sendTo || ''}${data?.options?.orgName || ''}${
+        data?.options?.tagName || ''
+      }通过微信发送${data?.options?.templateName || data?.notify?.templateId}`;
     case 'email':
-      return (
-        <div className={styles['notify-img-highlight']}>
-          向<span>{data?.options?.sendTo || ''}</span>
-          通过<span>邮件</span>发送
-          <span>{data?.options?.templateName || data?.notify?.templateId}</span>
-        </div>
-      );
+      return `向${data?.options?.sendTo || ''}通过邮件发送${
+        data?.options?.templateName || data?.notify?.templateId
+      }`;
     case 'voice':
-      return (
-        <div className={styles['notify-img-highlight']}>
-          向<span>{data?.options?.sendTo || ''}</span>
-          通过<span>语音</span>发送
-          <span>{data?.options?.templateName || data?.notify?.templateId}</span>
-        </div>
-      );
+      return `向${data?.options?.sendTo || ''}通过语音发送 ${
+        data?.options?.templateName || data?.notify?.templateId
+      }`;
     case 'sms':
-      return (
-        <div className={styles['notify-img-highlight']}>
-          向{data?.options?.sendTo || ''}通过短信发送
-          {data?.options?.templateName || data?.notify?.templateId}
-        </div>
-      );
+      return `向${data?.options?.sendTo || ''}通过短信发送${
+        data?.options?.templateName || data?.notify?.templateId
+      }`;
     case 'webhook':
-      return (
-        <div className={styles['notify-img-highlight']}>
-          通过<span>webhook</span>发送
-          <span>{data?.options?.templateName || data?.notify?.templateId}</span>
-        </div>
-      );
+      return `通过webhook发送${data?.options?.templateName || data?.notify?.templateId}`;
     default:
       return null;
   }
@@ -194,100 +82,55 @@ const notifyRender = (data: ActionsType | undefined) => {
 const deviceRender = (data: ActionsType | undefined) => {
   switch (data?.device?.selector) {
     case 'fixed':
-      return (
-        <div className={styles['notify-text-highlight']}>
-          {data?.options?.type}
-          <span className={styles['trigger-device']}>{data?.options?.name}</span>
-          {data?.options?.properties}
-        </div>
-      );
+      return `${data?.options?.type}${data?.options?.name}${data?.options?.properties}`;
     case 'tag':
-      return (
-        <div className={styles['notify-text-highlight']}>
-          {data?.options?.type}
-          {data.options?.taglist.map((item: any) => (
-            <span className={styles['trigger-device']}>
-              {item.type}
-              {item.name}
-              {item.value}
-            </span>
-          ))}
-          {data?.options?.productName}
-          {data?.options?.properties}
-        </div>
-      );
+      let tags: string = '';
+      data.options?.taglist.map((item: any) => {
+        tags += item.type || '' + item.name || '' + item.value || '';
+      });
+      return `${
+        data?.options?.type + tags + data?.options?.productName + data?.options?.properties
+      }`;
     case 'relation':
-      return (
-        <div className={styles['notify-text-highlight']}>
-          {data?.options?.type}与
-          <span className={styles['trigger-device']}>{data?.options?.name}</span>具有相同
-          {data?.options?.relationName}的{data?.options?.productName}设备的
-          {data?.options?.properties}
-        </div>
-      );
+      return `${data?.options?.type}与${data?.options?.name}具有相同${data?.options?.relationName}的${data?.options?.productName}设备的${data?.options?.properties}`;
     default:
       return null;
   }
 };
 
-const actionRender = (action: ActionsType, index: number) => {
+const actionRender = (action: ActionsType) => {
   switch (action?.executor) {
     case 'notify':
-      return (
-        <div
-          className={styles['card-item-content-action-item-right-item']}
-          key={action?.key || index}
-        >
-          <div className={classNames(styles['trigger-contents'], 'ellipsis')}>
-            {notifyRender(action)}
-          </div>
-        </div>
-      );
+      return notifyRender(action);
     case 'delay':
-      return (
-        <div
-          className={styles['card-item-content-action-item-right-item']}
-          key={action?.key || index}
-        >
-          <div className={classNames(styles['trigger-contents'], 'ellipsis')}>
-            <span style={{ fontWeight: 'bold' }}>
-              {action?.delay?.time}
-              {UnitEnum[action?.delay?.unit || '']}
-            </span>
-            后执行后续动作
-          </div>
-        </div>
-      );
+      return `${action?.delay?.time}${UnitEnum[action?.delay?.unit || '']}后执行后续动作`;
     case 'device':
-      return (
-        <div
-          className={styles['card-item-content-action-item-right-item']}
-          key={action?.key || index}
-        >
-          <div className={classNames(styles['trigger-contents'], 'ellipsis')}>
-            {deviceRender(action)}
-          </div>
-        </div>
-      );
+      return deviceRender(action);
     case 'alarm':
-      return (
-        <div
-          className={styles['card-item-content-action-item-right-item']}
-          key={action?.key || index}
-        >
-          <div className={classNames(styles['trigger-contents'], 'ellipsis')}>
-            满足条件后将触发关联此场景的告警
-          </div>
-        </div>
-      );
+      if (action?.alarm?.mode === 'relieve') {
+        return '满足条件后将解除关联此场景的告警';
+      }
+      return '满足条件后将触发关联此场景的告警';
     default:
       return null;
   }
 };
+// 过滤器
+const actionFilter = (terms: any, isLast: boolean, index: number) => {
+  if (isArray(terms)) {
+    return `动作${index + 1}${handleOptionsLabel(terms, isLast ? terms?.[0]?.type : undefined)}`;
+  }
+  return '';
+};
 
 const conditionsRender = (when: any[], index: number) => {
   let whenStr: string = '';
-  if (when.length && when[index]) {
+  if (
+    when.length &&
+    when[index] &&
+    (when[index]?.terms).length &&
+    when[index]?.terms[0]?.terms?.length
+  ) {
     const terms = when[index]?.terms || [];
     const termsLength = terms.length;
     terms.map((termsItem: any, tIndex: number) => {
@@ -298,11 +141,11 @@ const conditionsRender = (when: any[], index: number) => {
           ? termsItem.terms
               .map((bTermItem: any, bIndex: number) => {
                 const bLast = bIndex < termsItem.terms.length - 1;
-                return handleOptionsLabel(bTermItem, bLast ? '' : bTermItem[3]);
+                return handleOptionsLabel(bTermItem, !bLast ? '' : bTermItem[3]);
               })
               .join('')
           : '';
-        whenStr += tLast ? tSer : tLast + termsItem.termType;
+        whenStr += !tLast ? tSer : tSer + termsItem.termType;
       }
     });
     return whenStr;
@@ -312,28 +155,40 @@ const conditionsRender = (when: any[], index: number) => {
 
 const branchesActionRender = (actions: any[]) => {
   if (actions && actions?.length) {
-    const list: any[] = [];
-    actions.map((item, index) => {
-      const dt = actionRender(item, index);
-      list.push(dt);
-    });
-
-    return list.map((item, index) => (
-      <div className={styles['right-item-right-item-contents-item']}>
-        <div style={{ margin: '0 10px' }}>{item}</div>
-        <MyTooltip title={actions[index]?.options?.terms || ''}>
-          {actions[index]?.options?.terms && (
-            <div className={'ellipsis'} style={{ minWidth: 40 }}>
-              动作{index + 1}
-              {handleOptionsLabel(
-                actions[index]?.options?.terms,
-                index < actions.length - 1 ? actions[index].terms?.[0]?.type : undefined,
+    const list = actions.slice(0, 3);
+    return list.map((item, index) => {
+      const isLast = index < actions.length - 1;
+      return (
+        <div
+          className={styles['right-item-right-item-contents-item']}
+          style={{ maxWidth: `(${100 / list.length})%` }}
+        >
+          <MyTooltip placement={'topLeft'} title={actionRender(item)}>
+            <div
+              className={classNames(
+                styles['right-item-right-item-contents-item-action'],
+                styles['item-ellipsis'],
               )}
+            >
+              {actionRender(item)}
             </div>
-          )}
-        </MyTooltip>
-      </div>
-    ));
+          </MyTooltip>
+          <MyTooltip
+            placement={'topLeft'}
+            title={actionFilter(actions[index]?.options?.terms, isLast, index)}
+          >
+            <div
+              className={classNames(
+                styles['right-item-right-item-contents-item-filter'],
+                styles['item-ellipsis'],
+              )}
+            >
+              {actionFilter(actions[index]?.options?.terms, isLast, index)}
+            </div>
+          </MyTooltip>
+        </div>
+      );
+    });
   }
   return '';
 };
@@ -451,9 +306,28 @@ const ContentRender = (data: SceneCardProps) => {
                               <div className={styles['trigger-ways']}>
                                 {i ? (i.parallel ? '并行执行' : '串行执行') : ''}
                               </div>
-                              <div className={classNames(styles['right-item-right-item-contents'])}>
-                                {branchesActionRender(Array.isArray(i?.actions) ? i?.actions : [])}
-                              </div>
+                              {Array.isArray(i?.actions) && (
+                                <div
+                                  className={classNames(styles['right-item-right-item-contents'])}
+                                >
+                                  <div
+                                    className={classNames(
+                                      styles['right-item-right-item-contents-text'],
+                                    )}
+                                  >
+                                    {branchesActionRender(i?.actions)}
+                                  </div>
+                                  {i?.actions.length > 3 && (
+                                    <div
+                                      className={classNames(
+                                        styles['right-item-right-item-contents-extra'],
+                                      )}
+                                    >
+                                      等{i?.actions.length}个执行动作
+                                    </div>
+                                  )}
+                                </div>
+                              )}
                             </div>
                           ))}
                         </div>

+ 4 - 0
src/pages/account/Center/typings.d.ts

@@ -12,6 +12,10 @@ type UserItem = {
   roleList?: { id: string; name: string }[] | string[];
   orgIdList?: string[];
   roleIdList?: string[];
+  type?: {
+    name: string;
+    id: string;
+  };
 };
 type UserDetail = {
   name: string;

+ 78 - 40
src/pages/rule-engine/Scene/Save/action/ListItem/Item.tsx

@@ -6,8 +6,8 @@ import './index.less';
 import TriggerAlarm from '../TriggerAlarm';
 import { AddButton } from '@/pages/rule-engine/Scene/Save/components/Buttons';
 import FilterCondition from './FilterCondition';
-import { set } from 'lodash';
-import { Popconfirm } from 'antd';
+import { isArray, set } from 'lodash';
+import { Form, Popconfirm } from 'antd';
 
 export enum ParallelEnum {
   'parallel' = 'parallel',
@@ -306,45 +306,83 @@ export default (props: ItemProps) => {
         </Popconfirm>
       </div>
       {props.parallel ? null : (
-        <FilterCondition
-          action={props.name}
-          branchGroup={props.branchGroup}
-          thenName={props.thenName}
-          data={props.data.terms?.[0]}
-          label={props.data.options?.terms}
-          onAdd={() => {
-            let _data = props.data;
-            if (!_data.terms) {
-              _data = {
-                ..._data,
-                terms: [{}],
-              };
+        <Form.Item
+          name={[
+            'branches',
+            props.thenName,
+            'then',
+            props.branchGroup || 0,
+            'actions',
+            props.name,
+            'terms',
+          ]}
+          rules={[
+            {
+              validator(_, v) {
+                if (v) {
+                  if (!v.column) {
+                    return Promise.reject(new Error('请选择参数'));
+                  }
+
+                  if (!v.termType) {
+                    return Promise.reject(new Error('请选择操作符'));
+                  }
+
+                  if (!v.value) {
+                    return Promise.reject(new Error('请选择或输入参数值'));
+                  } else {
+                    if (isArray(v.value.value) && v.value.value.some((_v: any) => !_v)) {
+                      return Promise.reject(new Error('请选择或输入参数值'));
+                    } else if (!v.value.value) {
+                      return Promise.reject(new Error('请选择或输入参数值'));
+                    }
+                  }
+                }
+                return Promise.resolve();
+              },
+            },
+          ]}
+        >
+          <FilterCondition
+            action={props.name}
+            branchGroup={props.branchGroup}
+            thenName={props.thenName}
+            data={props.data.terms?.[0]}
+            label={props.data.options?.terms}
+            onAdd={() => {
+              let _data = props.data;
+              if (!_data.terms) {
+                _data = {
+                  ..._data,
+                  terms: [{}],
+                };
+                cacheValueRef.current = _data;
+                props.onUpdate(_data, op);
+              }
+            }}
+            onChange={(termsData) => {
+              const _data = props.data;
+              set(_data, 'terms', [termsData]);
               cacheValueRef.current = _data;
-              props.onUpdate(_data, op);
-            }
-          }}
-          onChange={(termsData) => {
-            const _data = props.data;
-            set(_data, 'terms', [termsData]);
-            cacheValueRef.current = _data;
-            props.onUpdate(_data, {
-              ...op,
-            });
-          }}
-          onLabelChange={(lb) => {
-            props.onUpdate(cacheValueRef.current, {
-              ...op,
-              terms: lb,
-            });
-          }}
-          onDelete={() => {
-            const _data = props.data;
-            if (_data.terms) {
-              delete _data.terms;
-              props.onUpdate(_data, op);
-            }
-          }}
-        />
+              props.onUpdate(_data, {
+                ...op,
+              });
+            }}
+            onLabelChange={(lb) => {
+              props.onUpdate(cacheValueRef.current, {
+                ...op,
+                terms: lb,
+              });
+            }}
+            onDelete={() => {
+              const _data = props.data;
+              if (_data.terms) {
+                delete _data.terms;
+                props.onUpdate(_data, op);
+              }
+            }}
+          />
+        </Form.Item>
       )}
       {visible && (
         <Modal

+ 0 - 2
src/pages/rule-engine/Scene/Save/action/ListItem/index.less

@@ -87,8 +87,6 @@
 }
 
 .filter-condition-warp {
-  margin-bottom: 16px;
-
   .filter-condition-content {
     position: relative;
     padding: 16px;

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

@@ -7,7 +7,7 @@ import { Observer } from '@formily/react';
 import { get } from 'lodash';
 import type { ShakeLimitType, BranchesThen } from '../../typings';
 import { randomString } from '@/utils/util';
-import { useEffect, useState } from 'react';
+import { useEffect, useRef, useState } from 'react';
 
 const { Panel } = Collapse;
 
@@ -17,14 +17,16 @@ interface ActionsProps {
   thenOptions: BranchesThen[];
   onAdd: (data: BranchesThen) => void;
   onUpdate: (data: BranchesThen, type: boolean) => void;
-  onChange?: (data: BranchesThen) => void;
+  onChange?: (data?: BranchesThen[]) => void;
 }
 
 export default (props: ActionsProps) => {
   const [parallelArray, setParallelArray] = useState<BranchesThen[]>([]); // 并行行
   const [serialArray, setSerialArray] = useState<BranchesThen[]>([]); // 串行
   const [activeKeys, setActiveKey] = useState<any | any[]>(['1']);
+
   const [lock, setLock] = useState(false);
+  const firstLockRef = useRef(true);
 
   useEffect(() => {
     const parallelArr = props.thenOptions.filter((item) => item.parallel);
@@ -35,6 +37,11 @@ export default (props: ActionsProps) => {
       setActiveKey(['2']);
       setLock(true);
     }
+    if (!firstLockRef.current) {
+      props.onChange?.(props.thenOptions);
+    } else {
+      firstLockRef.current = false;
+    }
   }, [props.thenOptions]);
 
   return (

+ 2 - 3
src/pages/rule-engine/Scene/Save/device/index.tsx

@@ -78,7 +78,7 @@ export default observer((props: Props) => {
             const label = handleLabel(FormModel.current.options?.trigger);
             return (
               <Form.Item
-                label={<TitleComponent style={{ fontSize: 14 }} data={'设备触发'} />}
+                label={<TitleComponent style={{ fontSize: 14 }} data={'触发规则'} />}
                 name={'device'}
                 rules={[
                   {
@@ -110,14 +110,13 @@ export default observer((props: Props) => {
           }}
         </Observer>
       </div>
-      <Terms />
+      <Terms form={props.form} />
       {visible && (
         <AddModel
           value={FormModel.current.trigger?.device || defaultDeviceValue}
           options={FormModel.current.options?.trigger}
           onSave={(data, options) => {
             setVisible(false);
-            console.log('FormModel.current.options', data);
             set(FormModel.current, ['options', 'trigger'], options);
             set(FormModel.current, ['trigger', 'device'], data);
             props.form.setFieldValue('device', data);

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

@@ -5,3 +5,21 @@
     color: rgba(#000, 0.8);
   }
 }
+
+.scene-header {
+  display: flex;
+  align-items: center;
+  justify-content: flex-start;
+
+  .scene-header-type {
+    display: flex;
+    align-items: center;
+    min-width: 100px;
+    margin-left: 16px;
+    padding: 4px 8px;
+    color: rgba(0, 0, 0, 0.65);
+    font-size: 14px;
+    border: 1px solid rgba(0, 0, 0, 0.2);
+    border-radius: 2px;
+  }
+}

+ 26 - 12
src/pages/rule-engine/Scene/Save/index.tsx

@@ -4,7 +4,7 @@ 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 { Ellipsis, TitleComponent } from '@/components';
 import { observable } from '@formily/reactive';
 import { observer } from '@formily/react';
 import type { FormModelType } from '@/pages/rule-engine/Scene/typings';
@@ -15,6 +15,7 @@ import { onlyMessage, randomString } from '@/utils/util';
 import { useHistory } from 'umi';
 import { getMenuPathByCode } from '@/utils/menu';
 import { cloneDeep, isArray } from 'lodash';
+import { TriggerWayType, iconMap } from '@/components/ProTableCard/CardItems/Scene';
 
 export const defaultBranches = [
   {
@@ -116,23 +117,23 @@ export default observer(() => {
     if (id) {
       service.detail(id).then((resp) => {
         if (resp.status === 200) {
+          const _triggerType = resp.result.triggerType;
           let branches = resp.result.branches;
           if (!branches) {
-            branches = defaultBranches;
-            if (resp.result.triggerType === 'device') {
+            branches = cloneDeep(defaultBranches);
+            if (_triggerType === 'device') {
               branches.push(null);
             }
           } else {
             const branchesLength = branches.length;
             if (
-              resp.result.triggerType === 'device' &&
+              _triggerType === 'device' &&
               ((branchesLength === 1 && !!branches[0]?.when?.length) || // 有一组数据并且when有值
                 (branchesLength > 1 && !branches[branchesLength - 1]?.when?.length)) // 有多组否则数据,并且最后一组when有值
             ) {
               branches.push(null);
             }
           }
-
           FormModel.current = {
             ...resp.result,
           };
@@ -141,8 +142,9 @@ export default observer(() => {
           FormModel.current.trigger = resp.result.trigger || {};
           FormModel.current.branches = newBranches;
           form.setFieldValue('description', resp.result.description);
-          if (['device', 'timer'].includes(resp.result.triggerType)) {
-            form.setFieldValue(resp.result.triggerType, resp.result.trigger);
+
+          if (['device', 'timer'].includes(_triggerType)) {
+            form.setFieldValue(_triggerType, resp.result.trigger[_triggerType]);
           }
           form.setFieldValue('branches', newBranches);
         }
@@ -156,11 +158,7 @@ export default observer(() => {
       case 'device':
         return <Device form={_form} />;
       case 'manual':
-        return (
-          <Form.Item label={<TitleComponent style={{ fontSize: 14 }} data={'手动触发'} />}>
-            <Manual form={_form} />
-          </Form.Item>
-        );
+        return <Manual form={_form} />;
       case 'timer':
         return <Timer form={_form} />;
       default:
@@ -205,6 +203,22 @@ export default observer(() => {
   return (
     <PageContainer>
       <Card>
+        <div className={'scene-header'}>
+          <Ellipsis
+            title={FormModel.current?.name}
+            style={{
+              fontSize: 20,
+              color: 'rgba(0, 0, 0, 0.8)',
+              fontWeight: 'bold',
+              maxWidth: '50%',
+              width: 'max-content',
+            }}
+          />
+          <div className={'scene-header-type'}>
+            <img height={16} src={iconMap.get(triggerType)} style={{ marginRight: 8 }} />
+            {TriggerWayType[triggerType]}
+          </div>
+        </div>
         <Form layout={'vertical'} form={form}>
           {triggerRender(triggerType, form)}
           <Form.Item

+ 3 - 12
src/pages/rule-engine/Scene/Save/manual/index.tsx

@@ -1,12 +1,9 @@
 import Action from '../action';
 import { Observer } from '@formily/react';
 import { FormModel } from '@/pages/rule-engine/Scene/Save';
-import { Form, FormInstance } from 'antd';
-interface Props {
-  form: FormInstance;
-}
+import { Form } from 'antd';
 
-export default (props: Props) => {
+export default () => {
   return (
     <div>
       <Observer>
@@ -16,7 +13,7 @@ export default (props: Props) => {
             rules={[
               {
                 validator(_, v) {
-                  if (v && !v.length) {
+                  if (!v || (v && !v.length)) {
                     return Promise.reject('至少配置一个执行动作');
                   }
                   return Promise.resolve();
@@ -31,8 +28,6 @@ export default (props: Props) => {
                 if (FormModel.current.branches && data) {
                   const newThen = [...FormModel.current.branches[0].then, data];
                   FormModel.current.branches[0].then = newThen;
-                  props.form.setFieldValue(['branches', 0, 'then'], newThen);
-                  props.form.validateFields(['branches', 0, 'then']);
                 }
               }}
               onUpdate={(data, type) => {
@@ -45,10 +40,6 @@ export default (props: Props) => {
                   } else {
                     FormModel.current.branches![0].then = [];
                   }
-                  props.form.setFieldValue(
-                    ['branches', 0, 'then'],
-                    FormModel.current.branches![0].then,
-                  );
                 }
               }}
             />

+ 41 - 23
src/pages/rule-engine/Scene/Save/terms/branchItem.tsx

@@ -8,7 +8,7 @@ import Actions from '@/pages/rule-engine/Scene/Save/action';
 import classNames from 'classnames';
 import { set } from 'lodash';
 import { Store } from 'jetlinks-store';
-import { Popconfirm } from 'antd';
+import { Form, FormInstance, Popconfirm } from 'antd';
 
 interface BranchesItemProps {
   name: number;
@@ -17,6 +17,8 @@ interface BranchesItemProps {
   paramsOptions: any[];
   onDelete: () => void;
   onDeleteAll?: () => void;
+  form: FormInstance;
+  className?: string;
 }
 
 export default observer((props: BranchesItemProps) => {
@@ -64,7 +66,7 @@ export default observer((props: BranchesItemProps) => {
   };
 
   return (
-    <div className="actions-terms-warp">
+    <div className={classNames('actions-terms-warp', props.className)}>
       <div className="actions-terms-title">{props.isFirst ? '当' : '否则'}</div>
       <div
         className={classNames('actions-terms-options', { border: !props.isFirst, error: error })}
@@ -140,27 +142,43 @@ export default observer((props: BranchesItemProps) => {
           <Observer>
             {() => {
               return (
-                <Actions
-                  openShakeLimit={true}
-                  name={props.name}
-                  thenOptions={props.data.then}
-                  onAdd={(data) => {
-                    if (FormModel.current.branches && data) {
-                      FormModel.current.branches[props.name].then = [
-                        ...FormModel.current.branches[props.name].then,
-                        data,
-                      ];
-                    }
-                  }}
-                  onUpdate={(data, type) => {
-                    const indexOf = FormModel.current.branches![props.name].then.findIndex(
-                      (item) => item.parallel === type,
-                    );
-                    if (indexOf !== -1) {
-                      FormModel.current.branches![props.name].then[indexOf] = data;
-                    }
-                  }}
-                />
+                <Form.Item
+                  name={['branches', props.name, 'then']}
+                  rules={[
+                    {
+                      validator(_, v) {
+                        if (!v || (v && !v.length)) {
+                          return Promise.reject('至少配置一个执行动作');
+                        }
+                        return Promise.resolve();
+                      },
+                    },
+                  ]}
+                >
+                  <Actions
+                    openShakeLimit={true}
+                    name={props.name}
+                    thenOptions={props.data.then}
+                    onAdd={(data) => {
+                      if (FormModel.current.branches && data) {
+                        const newThen = [...FormModel.current.branches[props.name].then, data];
+                        FormModel.current.branches[props.name].then = newThen;
+                      }
+                    }}
+                    onUpdate={(data, type) => {
+                      const indexOf = FormModel.current.branches![props.name].then.findIndex(
+                        (item) => item.parallel === type,
+                      );
+                      if (indexOf !== -1) {
+                        if (data.actions?.length) {
+                          FormModel.current.branches![props.name].then[indexOf] = data;
+                        } else {
+                          FormModel.current.branches![props.name].then = [];
+                        }
+                      }
+                    }}
+                  />
+                </Form.Item>
               );
             }}
           </Observer>

+ 14 - 3
src/pages/rule-engine/Scene/Save/terms/index.less

@@ -30,9 +30,11 @@
 .actions-terms {
   .actions-terms-warp {
     display: flex;
+    margin-bottom: 24px;
 
-    &:not(:last-child) {
-      margin-bottom: 24px;
+    &.first-children,
+    &:last-child {
+      margin-bottom: 0;
     }
 
     .actions-terms-title {
@@ -51,7 +53,7 @@
       width: 0;
 
       &.border {
-        padding: 10px 18px 18px 18px;
+        padding: 10px 18px 0 18px;
         border: 1px dashed #999;
         border-radius: 2px;
       }
@@ -62,6 +64,13 @@
 
       .actions-terms-list {
         position: relative;
+        margin-bottom: 16px;
+
+        .ant-form-item-has-error {
+          .params-item_button {
+            border-color: @error-color;
+          }
+        }
 
         .actions-terms-list-content {
           display: flex;
@@ -103,6 +112,7 @@
     display: flex;
     // flex-wrap: wrap;
     padding: 8px;
+    padding-bottom: 0;
     background-color: #fafafa;
     row-gap: 16px;
     .terms-params-item {
@@ -156,6 +166,7 @@
     padding: 4px;
     border: 1px solid rgba(0, 0, 0, 0.1);
     border-radius: 2px;
+    transition: border 0.3s;
 
     // > div {
     //   flex-shrink: 0;

+ 71 - 36
src/pages/rule-engine/Scene/Save/terms/index.tsx

@@ -1,11 +1,11 @@
-import { useEffect, useState } from 'react';
+import { useCallback, useEffect, useState } from 'react';
 import { TitleComponent } from '@/components';
 import { observer, Observer } from '@formily/react';
 import { model } from '@formily/reactive';
 import { FormModel, defaultBranches } from '@/pages/rule-engine/Scene/Save';
 import BranchItem from './branchItem';
 import { service } from '@/pages/rule-engine/Scene/index';
-import { Form, Switch } from 'antd';
+import { Form, FormInstance, Switch } from 'antd';
 import type { TriggerType } from '@/pages/rule-engine/Scene/typings';
 import Actions from '@/pages/rule-engine/Scene/Save/action';
 import { cloneDeep, set } from 'lodash';
@@ -21,7 +21,11 @@ export const TermsModel = model<TermsModelProps>({
   columnOptions: [],
 });
 
-export default observer(() => {
+interface Props {
+  form: FormInstance;
+}
+
+export default observer((props: Props) => {
   const [open, setOpen] = useState(true);
 
   useEffect(() => {
@@ -46,37 +50,43 @@ export default observer(() => {
     });
   };
 
-  const openChange = (checked: boolean) => {
-    setOpen(checked);
-    const key = randomString();
-    if (checked) {
-      FormModel.current.branches = cloneDeep([...defaultBranches, null as any]);
-      set(FormModel.current.options!, 'when', [
-        {
-          terms: [
-            {
-              terms: [],
+  const openChange = useCallback(
+    (checked: boolean) => {
+      setOpen(checked);
+      const key = randomString();
+      if (checked) {
+        FormModel.current.branches = cloneDeep([...defaultBranches, null as any]);
+        set(FormModel.current.options!, 'when', [
+          {
+            terms: [
+              {
+                terms: [],
+              },
+            ],
+          },
+        ]);
+        props.form.setFieldValue(['branches'], [...defaultBranches]);
+      } else {
+        const newValue = [
+          {
+            when: [],
+            key: 'branches_' + key,
+            shakeLimit: {
+              enabled: false,
+              time: 0,
+              threshold: 0,
+              alarmFirst: false,
             },
-          ],
-        },
-      ]);
-    } else {
-      FormModel.current.branches = [
-        {
-          when: [],
-          key: 'branches_' + key,
-          shakeLimit: {
-            enabled: false,
-            time: 0,
-            threshold: 0,
-            alarmFirst: false,
+            then: [],
           },
-          then: [],
-        },
-      ];
-      set(FormModel.current.options!, 'when', []);
-    }
-  };
+        ];
+        FormModel.current.branches = newValue;
+        set(FormModel.current.options!, 'when', []);
+        props.form.setFieldValue(['branches'], newValue);
+      }
+    },
+    [props.form],
+  );
 
   const addBranches = () => {
     const key = randomString();
@@ -123,8 +133,10 @@ export default observer(() => {
               const isFirst = index === 0;
               return item ? (
                 <BranchItem
+                  form={props.form}
                   data={item}
                   isFirst={isFirst}
+                  className={isFirst ? 'first-children' : ''}
                   name={index}
                   key={item.key}
                   paramsOptions={TermsModel.columnOptions}
@@ -145,7 +157,13 @@ export default observer(() => {
                   }}
                 />
               ) : (
-                <div className="actions-terms-warp" style={{ alignItems: 'center' }}>
+                <div
+                  className="actions-terms-warp"
+                  style={{
+                    alignItems: 'center',
+                    marginTop: FormModel.current.branches?.length === 2 ? 0 : 24,
+                  }}
+                >
                   <div className="actions-terms-title" style={{ padding: 0 }}>
                     否则
                   </div>
@@ -162,14 +180,27 @@ export default observer(() => {
           }
         </Observer>
       ) : (
-        <Form.Item>
+        <Form.Item
+          name={['branches', 0, 'then']}
+          rules={[
+            {
+              validator(_, v) {
+                if (!v || (v && !v.length)) {
+                  return Promise.reject('至少配置一个执行动作');
+                }
+                return Promise.resolve();
+              },
+            },
+          ]}
+        >
           <Actions
             openShakeLimit={true}
             name={0}
             thenOptions={FormModel.current.branches ? FormModel.current.branches[0].then : []}
             onAdd={(data) => {
               if (FormModel.current.branches && data) {
-                FormModel.current.branches[0].then = [...FormModel.current.branches[0].then, data];
+                const newThen = [...FormModel.current.branches[0].then, data];
+                FormModel.current.branches[0].then = newThen;
               }
             }}
             onUpdate={(data, type) => {
@@ -177,7 +208,11 @@ export default observer(() => {
                 (item) => item.parallel === type,
               );
               if (indexOf !== -1) {
-                FormModel.current.branches![0].then[indexOf] = data;
+                if (data.actions?.length) {
+                  FormModel.current.branches![0].then[indexOf] = data;
+                } else {
+                  FormModel.current.branches![0].then = [];
+                }
               }
             }}
           />

+ 15 - 5
src/pages/rule-engine/Scene/Save/terms/paramsItem.tsx

@@ -20,6 +20,7 @@ interface ParamsItemProps {
   isDelete: boolean;
   options: any[];
   onValueChange?: (value: TermsType) => void;
+  onChange?: (value: TermsType) => void;
   onLabelChange?: (label: string[]) => void;
   label?: any[];
   onAdd: () => void;
@@ -108,6 +109,7 @@ const ParamsItem = observer((props: ParamsItemProps) => {
   const [termType, setTermType] = useState('');
   const [column, setColumn] = useState('');
   const labelCache = useRef<any[]>([undefined, undefined, {}, 'and']);
+  const firstLockRef = useRef(true);
 
   const ValueRef = useRef<Partial<TermsType>>({
     column: '',
@@ -164,6 +166,11 @@ const ParamsItem = observer((props: ParamsItemProps) => {
     setColumn(props.data.column || '');
     ValueRef.current = props.data || {};
     convertLabelValue(props.data.column);
+    if (!firstLockRef.current) {
+      props.onChange?.(props.data);
+    } else {
+      firstLockRef.current = false;
+    }
   }, [props.data]);
 
   useEffect(() => {
@@ -171,7 +178,6 @@ const ParamsItem = observer((props: ParamsItemProps) => {
   }, [props.options]);
 
   useEffect(() => {
-    console.log('params-effect', props.label);
     labelCache.current = props.label || [undefined, undefined, {}, 'and'];
   }, [props.label]);
 
@@ -273,7 +279,8 @@ const ParamsItem = observer((props: ParamsItemProps) => {
                 ValueRef.current.value = _myValue;
                 setValue(_myValue);
                 labelCache.current[2] = { ...labelCache.current[2], 0: lb };
-                props.onLabelChange?.([...labelCache.current, props.data.type]);
+                labelCache.current[3] = props.data.type;
+                props.onLabelChange?.([...labelCache.current]);
                 valueEventChange(_myValue);
               }}
             />
@@ -293,7 +300,8 @@ const ParamsItem = observer((props: ParamsItemProps) => {
                 ValueRef.current.value = _myValue;
                 setValue(_myValue);
                 labelCache.current[2] = { ...labelCache.current[2], 1: lb };
-                props.onLabelChange?.([...labelCache.current, props.data.type]);
+                labelCache.current[3] = props.data.type;
+                props.onLabelChange?.([...labelCache.current]);
                 valueEventChange(_myValue);
               }}
             />
@@ -313,7 +321,8 @@ const ParamsItem = observer((props: ParamsItemProps) => {
               ValueRef.current.value = v;
               labelCache.current[2] = { 0: lb };
               console.log('valueChange', labelCache.current);
-              props.onLabelChange?.([...labelCache.current, props.data.type]);
+              labelCache.current[3] = props.data.type;
+              props.onLabelChange?.([...labelCache.current]);
               valueEventChange(v);
             }}
           />
@@ -336,7 +345,8 @@ const ParamsItem = observer((props: ParamsItemProps) => {
             value={props.data.type}
             onChange={(v) => {
               props.data.type = v;
-              props.onLabelChange?.([...labelCache.current, v]);
+              labelCache.current[3] = v;
+              props.onLabelChange?.([...labelCache.current]);
             }}
           />
         </div>

+ 87 - 55
src/pages/rule-engine/Scene/Save/terms/term.tsx

@@ -6,9 +6,9 @@ import { CloseOutlined, PlusOutlined } from '@ant-design/icons';
 import { DropdownButton } from '@/pages/rule-engine/Scene/Save/components/Buttons';
 import classNames from 'classnames';
 import type { TermsType } from '@/pages/rule-engine/Scene/typings';
-import { get, set } from 'lodash';
+import { get, isArray, set } from 'lodash';
 import './index.less';
-import { Popconfirm } from 'antd';
+import { Form, Popconfirm } from 'antd';
 
 interface TermsProps {
   data: TermsType;
@@ -66,61 +66,93 @@ export default observer((props: TermsProps) => {
               const _when = get(FormModel.current.branches, [...props.pName, props.name]);
               const terms: TermsType[] = _when?.terms || [];
               return terms.map((item, index) => (
-                <ParamsItem
-                  pName={[...props.pName, props.name]}
-                  isDelete={terms.length > 1}
-                  name={index}
-                  data={item}
-                  key={item.key}
-                  isLast={index === props.data.terms!.length - 1}
-                  options={props.paramsOptions}
-                  label={
-                    FormModel.current.options?.when?.[props.whenName]?.terms?.[props.name]?.terms?.[
-                      index
-                    ]
-                  }
-                  onDelete={() => {
-                    terms.splice(index, 1);
-                    // setTerms([...terms]);
-                    // props.onValueChange({
-                    //   terms: terms,
-                    // });
-                  }}
-                  onAdd={() => {
-                    const key = `params_${new Date().getTime()}`;
-                    terms.push({
-                      type: 'and',
-                      column: undefined,
-                      value: undefined,
-                      termType: undefined,
-                      key,
-                    });
+                <Form.Item
+                  name={['branches', ...props.pName, props.name, 'terms', index]}
+                  rules={[
+                    {
+                      validator(_, v) {
+                        if (v) {
+                          if (!v.column) {
+                            return Promise.reject(new Error('请选择参数'));
+                          }
 
-                    // setTerms([...terms]);
-                    props.onValueChange({
-                      ..._when,
-                      terms: terms,
-                    });
-                  }}
-                  onValueChange={(data) => {
-                    terms[index] = {
-                      ...terms[index],
-                      ...data,
-                    };
+                          if (!v.termType) {
+                            return Promise.reject(new Error('请选择操作符'));
+                          }
 
-                    // setTerms([...terms]);
-                    props.onValueChange({
-                      ..._when,
-                      terms: terms,
-                    });
-                  }}
-                  onLabelChange={(options) => {
-                    FormModel.current.options!.when[props.whenName].terms[props.name].terms[index] =
-                      options;
-                    FormModel.current.options!.when[props.whenName].terms[props.name].termType =
-                      props.data.type === 'and' ? '并且' : '或者';
-                  }}
-                />
+                          if (!v.value) {
+                            return Promise.reject(new Error('请选择或输入参数值'));
+                          } else {
+                            if (isArray(v.value.value) && v.value.value.some((_v: any) => !_v)) {
+                              return Promise.reject(new Error('请选择或输入参数值'));
+                            } else if (!v.value.value) {
+                              return Promise.reject(new Error('请选择或输入参数值'));
+                            }
+                          }
+                        } else {
+                          return Promise.reject(new Error('请选择参数'));
+                        }
+                        return Promise.resolve();
+                      },
+                    },
+                  ]}
+                >
+                  <ParamsItem
+                    pName={[...props.pName, props.name]}
+                    isDelete={terms.length > 1}
+                    name={index}
+                    data={item}
+                    key={item.key}
+                    isLast={index === props.data.terms!.length - 1}
+                    options={props.paramsOptions}
+                    label={
+                      FormModel.current.options?.when?.[props.whenName]?.terms?.[props.name]
+                        ?.terms?.[index]
+                    }
+                    onDelete={() => {
+                      terms.splice(index, 1);
+                      // setTerms([...terms]);
+                      // props.onValueChange({
+                      //   terms: terms,
+                      // });
+                    }}
+                    onAdd={() => {
+                      const key = `params_${new Date().getTime()}`;
+                      terms.push({
+                        type: 'and',
+                        column: undefined,
+                        value: undefined,
+                        termType: undefined,
+                        key,
+                      });
+
+                      // setTerms([...terms]);
+                      props.onValueChange({
+                        ..._when,
+                        terms: terms,
+                      });
+                    }}
+                    onValueChange={(data) => {
+                      terms[index] = {
+                        ...terms[index],
+                        ...data,
+                      };
+
+                      // setTerms([...terms]);
+                      props.onValueChange({
+                        ..._when,
+                        terms: terms,
+                      });
+                    }}
+                    onLabelChange={(options) => {
+                      FormModel.current.options!.when[props.whenName].terms[props.name].terms[
+                        index
+                      ] = options;
+                      FormModel.current.options!.when[props.whenName].terms[props.name].termType =
+                        props.data.type === 'and' ? '并且' : '或者';
+                    }}
+                  />
+                </Form.Item>
               ));
             }}
           </Observer>

+ 2 - 8
src/pages/rule-engine/Scene/Save/timer/index.tsx

@@ -40,7 +40,7 @@ export default observer((props: Props) => {
             const label = handleLabel(FormModel.current.options?.trigger);
             return (
               <Form.Item
-                label={<TitleComponent style={{ fontSize: 14 }} data={'定时触发'} />}
+                label={<TitleComponent style={{ fontSize: 14 }} data={'触发规则'} />}
                 name={'timer'}
                 rules={[
                   {
@@ -80,7 +80,7 @@ export default observer((props: Props) => {
               rules={[
                 {
                   validator(_, v) {
-                    if (v && !v.length) {
+                    if (!v || (v && !v.length)) {
                       return Promise.reject('至少配置一个执行动作');
                     }
                     return Promise.resolve();
@@ -95,8 +95,6 @@ export default observer((props: Props) => {
                   if (FormModel.current.branches && data) {
                     const newThen = [...FormModel.current.branches[0].then, data];
                     FormModel.current.branches[0].then = newThen;
-                    props.form.setFieldValue(['branches', 0, 'then'], newThen);
-                    props.form.validateFields(['branches', 0, 'then']);
                   }
                 }}
                 onUpdate={(data, type) => {
@@ -109,10 +107,6 @@ export default observer((props: Props) => {
                     } else {
                       FormModel.current.branches![0].then = [];
                     }
-                    props.form.setFieldValue(
-                      ['branches', 0, 'then'],
-                      FormModel.current.branches![0].then,
-                    );
                   }
                 }}
               />

+ 13 - 7
src/pages/system/User/index.tsx

@@ -91,13 +91,19 @@ const User = observer(() => {
       },
       hideInSearch: false,
     },
-    // {
-    //   title: '用户类型',
-    //   dataIndex: 'type',
-    //   render: (_, record) => (
-    //     <Tag color={typeMap.get('name')}>{record.name}</Tag>
-    //   ),
-    // },
+    {
+      title: '用户类型',
+      dataIndex: 'type',
+      render: (_, row) => row.type?.name || '',
+      valueType: 'select',
+      request: () =>
+        service.queryUserType().then((resp: any) =>
+          resp.result.map((item: any) => ({
+            label: item.name,
+            value: item.id,
+          })),
+        ),
+    },
     {
       title: intl.formatMessage({
         id: 'pages.searchTable.titleStatus',

+ 4 - 0
src/pages/system/User/serivce.ts

@@ -54,6 +54,10 @@ class Service extends BaseService<UserItem> {
       method: 'POST',
       data: password,
     });
+  queryUserType = () =>
+    request(`/${SystemConst.API_BASE}/user/detail/types`, {
+      method: 'GET',
+    });
 }
 
 export default Service;

+ 4 - 0
src/pages/system/User/typings.d.ts

@@ -13,4 +13,8 @@ type UserItem = {
   roleList?: { id: string; name: string }[] | string[];
   orgIdList?: string[];
   roleIdList?: string[];
+  type?: {
+    name: string;
+    id: string;
+  };
 };