index.tsx 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489
  1. import React, { useState } from 'react';
  2. import { StatusColorEnum } from '@/components/BadgeStatus';
  3. import { Ellipsis, TableCard } from '@/components';
  4. import '@/style/common.less';
  5. import '../../index.less';
  6. import styles from './index.less';
  7. import type { SceneItem } from '@/pages/rule-engine/Scene/typings';
  8. import { CheckOutlined, DownOutlined, UpOutlined } from '@ant-design/icons';
  9. import classNames from 'classnames';
  10. import { ActionsType, BranchesThen } from '@/pages/rule-engine/Scene/typings';
  11. import MyTooltip from './MyTooltip';
  12. import { handleOptionsLabel } from '@/pages/rule-engine/Scene/Save/terms/paramsItem';
  13. import { isArray } from 'lodash';
  14. const imageMap = new Map();
  15. imageMap.set('timer', require('/public/images/scene/scene-timer.png'));
  16. imageMap.set('manual', require('/public/images/scene/scene-hand.png'));
  17. imageMap.set('device', require('/public/images/scene/scene-device.png'));
  18. export const iconMap = new Map();
  19. iconMap.set('timer', require('/public/images/scene/trigger-type-icon/timing.png'));
  20. iconMap.set('manual', require('/public/images/scene/trigger-type-icon/manual.png'));
  21. iconMap.set('device', require('/public/images/scene/trigger-type-icon/device.png'));
  22. // @ts-ignore
  23. export interface SceneCardProps extends SceneItem {
  24. detail?: React.ReactNode;
  25. tools?: React.ReactNode[];
  26. avatarSize?: number;
  27. className?: string;
  28. onUnBind?: (e: any) => void;
  29. showBindBtn?: boolean;
  30. cardType?: 'bind' | 'unbind';
  31. showTool?: boolean;
  32. onClick?: () => void;
  33. }
  34. export enum TriggerWayType {
  35. manual = '手动触发',
  36. timer = '定时触发',
  37. device = '设备触发',
  38. }
  39. enum UnitEnum {
  40. seconds = '秒',
  41. minutes = '分',
  42. hours = '小时',
  43. }
  44. const notifyRender = (data: ActionsType | undefined) => {
  45. switch (data?.notify?.notifyType) {
  46. case 'dingTalk':
  47. if (data?.options?.provider === 'dingTalkRobotWebHook') {
  48. return `通过群机器人消息发送${data?.options?.templateName || data?.notify?.templateId}`;
  49. }
  50. return `通过钉钉向${data?.options?.notifierName || data?.notify?.notifierId}发送${
  51. data?.options?.templateName || data?.notify?.templateId
  52. }`;
  53. case 'weixin':
  54. return `通过微信向${data?.options?.sendTo || ''}${data?.options?.orgName || ''}${
  55. data?.options?.tagName || ''
  56. }发送${data?.options?.templateName || data?.notify?.templateId}`;
  57. case 'email':
  58. return `通过邮件向${data?.options?.sendTo || ''}发送${
  59. data?.options?.templateName || data?.notify?.templateId
  60. }`;
  61. case 'voice':
  62. return `通过语音向${data?.options?.sendTo || ''}发送 ${
  63. data?.options?.templateName || data?.notify?.templateId
  64. }`;
  65. case 'sms':
  66. return `通过短信向${data?.options?.sendTo || ''}发送${
  67. data?.options?.templateName || data?.notify?.templateId
  68. }`;
  69. case 'webhook':
  70. return `通过webhook发送${data?.options?.templateName || data?.notify?.templateId}`;
  71. default:
  72. return null;
  73. }
  74. };
  75. const deviceRender = (data: ActionsType | undefined) => {
  76. switch (data?.device?.selector) {
  77. case 'fixed':
  78. return `${data?.options?.type}${data?.options?.name}${data?.options?.properties}`;
  79. case 'tag':
  80. let tags: string = '';
  81. data.options?.taglist?.map((item: any) => {
  82. tags += item.type || '' + item.name || '' + item.value || '';
  83. });
  84. return `${
  85. data?.options?.type + tags + data?.options?.productName + data?.options?.properties
  86. }`;
  87. case 'relation':
  88. return `${data?.options?.type}与${data?.options?.triggerName}具有相同${data?.options?.relationName}的${data?.options?.productName}设备的${data?.options?.properties}`;
  89. default:
  90. return null;
  91. }
  92. };
  93. const actionRender = (action: ActionsType) => {
  94. switch (action?.executor) {
  95. case 'notify':
  96. return notifyRender(action);
  97. case 'delay':
  98. return `${action?.delay?.time}${UnitEnum[action?.delay?.unit || '']}后执行后续动作`;
  99. case 'device':
  100. return deviceRender(action);
  101. case 'alarm':
  102. if (action?.alarm?.mode === 'relieve') {
  103. return '满足条件后将解除关联此场景的告警';
  104. }
  105. return '满足条件后将触发关联此场景的告警';
  106. default:
  107. return null;
  108. }
  109. };
  110. // 过滤器
  111. const actionFilter = (terms: any, isLast: boolean, index: number) => {
  112. if (!Array.isArray(terms)) return '';
  113. let str = `动作${index + 1} `;
  114. terms?.forEach((item: any, iindex: number) => {
  115. if (isArray(item.terms)) {
  116. item.terms.map((iItem: number, iIndex: number) => {
  117. const _isLast = iIndex < item.terms.length - 1;
  118. str += `${handleOptionsLabel(iItem, _isLast ? item.terms[iIndex + 1]?.[3] : undefined)}`;
  119. });
  120. }
  121. str += iindex < terms.length - 1 ? terms[iindex + 1].termType : '';
  122. });
  123. return str;
  124. };
  125. const conditionsRender = (when: any[], index: number) => {
  126. let whenStr: string = '';
  127. if (
  128. when.length &&
  129. when[index] &&
  130. (when[index]?.terms).length &&
  131. when[index]?.terms[0]?.terms?.length
  132. ) {
  133. const terms = when[index]?.terms || [];
  134. const termsLength = terms.length;
  135. terms.map((termsItem: any, tIndex: number) => {
  136. const tLast = tIndex < termsLength - 1;
  137. let tSer = '';
  138. if (termsItem.terms) {
  139. tSer = isArray(termsItem.terms)
  140. ? termsItem.terms
  141. .map((bTermItem: any, bIndex: number) => {
  142. const bLast = bIndex < termsItem.terms.length - 1;
  143. return handleOptionsLabel(bTermItem, bLast ? termsItem.terms[bIndex + 1]?.[3] : '');
  144. })
  145. .join('')
  146. : '';
  147. whenStr += !tLast ? tSer : tSer + terms[tIndex + 1].termType;
  148. }
  149. });
  150. return whenStr;
  151. }
  152. return whenStr;
  153. };
  154. const branchesActionRender = (actions: any[]) => {
  155. if (actions && actions?.length) {
  156. const list = actions.slice(0, 3);
  157. return list.map((item, index) => {
  158. const isLast = index < actions.length - 1;
  159. return (
  160. <div
  161. className={styles['right-item-right-item-contents-item']}
  162. style={{ maxWidth: `(${100 / list.length})%` }}
  163. >
  164. <MyTooltip placement={'topLeft'} title={actionRender(item)}>
  165. <div
  166. className={classNames(
  167. styles['right-item-right-item-contents-item-action'],
  168. styles['item-ellipsis'],
  169. )}
  170. >
  171. {actionRender(item)}
  172. </div>
  173. </MyTooltip>
  174. <MyTooltip
  175. placement={'topLeft'}
  176. title={actionFilter(actions[index]?.options?.terms, isLast, index)}
  177. >
  178. <div
  179. className={classNames(
  180. styles['right-item-right-item-contents-item-filter'],
  181. styles['item-ellipsis'],
  182. )}
  183. >
  184. {actionFilter(actions[index]?.options?.terms, isLast, index)}
  185. </div>
  186. </MyTooltip>
  187. </div>
  188. );
  189. });
  190. }
  191. return '';
  192. };
  193. const ContentRender = (data: SceneCardProps) => {
  194. const [visible, setVisible] = useState<boolean>(false);
  195. const type = data.triggerType;
  196. if (!!type && (data.branches || [])?.length) {
  197. const trigger = data?.options?.trigger;
  198. return (
  199. <div className={styles['card-item-content-box']}>
  200. <div className={classNames(styles['card-item-content-trigger'])}>
  201. {trigger?.name && (
  202. <MyTooltip placement="topLeft" title={trigger?.name || ''}>
  203. <div
  204. className={classNames(styles['card-item-content-trigger-item'], 'ellipsis')}
  205. style={{ maxWidth: '15%', color: 'rgba(47, 84, 235)' }}
  206. >
  207. {trigger?.name || ''}
  208. </div>
  209. </MyTooltip>
  210. )}
  211. {trigger?.productName && (
  212. <MyTooltip placement="topLeft" title={trigger?.productName || ''}>
  213. <div
  214. className={classNames(styles['card-item-content-trigger-item'], 'ellipsis')}
  215. style={{ maxWidth: '15%', color: 'rgba(47, 84, 235)' }}
  216. >
  217. {trigger?.productName || ''}
  218. </div>
  219. </MyTooltip>
  220. )}
  221. {trigger?.when && (
  222. <MyTooltip placement="topLeft" title={trigger?.when || ''}>
  223. <div
  224. className={classNames(styles['card-item-content-trigger-item'], 'ellipsis')}
  225. style={{ maxWidth: '16%' }}
  226. >
  227. {trigger?.when || ''}
  228. </div>
  229. </MyTooltip>
  230. )}
  231. {trigger?.time && (
  232. <div className={classNames(styles['card-item-content-trigger-item'])}>
  233. {trigger?.time || ''}
  234. </div>
  235. )}
  236. {trigger?.extraTime && (
  237. <div className={classNames(styles['card-item-content-trigger-item'])}>
  238. {trigger?.extraTime || ''}
  239. </div>
  240. )}
  241. {trigger?.action && (
  242. <MyTooltip placement="topLeft" title={trigger?.action || ''}>
  243. <div
  244. className={classNames(styles['card-item-content-trigger-item'], 'ellipsis')}
  245. style={{ maxWidth: '15%' }}
  246. >
  247. {trigger?.action || ''}
  248. </div>
  249. </MyTooltip>
  250. )}
  251. {trigger?.type && (
  252. <div className={classNames(styles['card-item-content-trigger-item'])}>
  253. {trigger?.type || ''}
  254. </div>
  255. )}
  256. </div>
  257. <div className={styles['card-item-content-action']}>
  258. {(visible ? data.branches || [] : (data?.branches || []).slice(0, 2)).map(
  259. (item: any, index) => {
  260. return (
  261. <div className={styles['card-item-content-action-item']} key={item?.key || index}>
  262. {type === 'device' && (
  263. <div className={styles['card-item-content-action-item-left']}>
  264. {index === 0 ? '当' : '否则'}
  265. </div>
  266. )}
  267. <div
  268. style={{ width: type === 'device' ? 'calc(100% - 48px)' : '100%' }}
  269. className={styles['card-item-content-action-item-right']}
  270. >
  271. <div className={styles['card-item-content-action-item-right-item']}>
  272. {type === 'device' &&
  273. (item.shakeLimit?.enabled || data.options?.when?.[index]) && (
  274. <div
  275. className={styles['right-item-left']}
  276. style={{
  277. width: Array.isArray(item.then) && item?.then.length ? '15%' : '100%',
  278. }}
  279. >
  280. <MyTooltip
  281. placement={'topLeft'}
  282. title={conditionsRender(data.options?.when || [], index)}
  283. >
  284. <div className={classNames(styles['trigger-conditions'], 'ellipsis')}>
  285. {conditionsRender(data.options?.when || [], index)}
  286. </div>
  287. </MyTooltip>
  288. {item.shakeLimit?.enabled && (
  289. <MyTooltip
  290. title={`(${item.shakeLimit?.time}秒内发生${item.shakeLimit?.threshold}
  291. 次以上时执行一次)`}
  292. >
  293. <div className={classNames(styles['trigger-shake'], 'ellipsis')}>
  294. ({item.shakeLimit?.time}秒内发生{item.shakeLimit?.threshold}
  295. 次以上时执行一次)
  296. </div>
  297. </MyTooltip>
  298. )}
  299. </div>
  300. )}
  301. {Array.isArray(item.then) && item?.then.length ? (
  302. <div
  303. className={styles['right-item-right']}
  304. style={{ width: type === 'device' ? '85%' : '100%' }}
  305. >
  306. {(item?.then || []).map((i: BranchesThen, _index: number) => {
  307. if (Array.isArray(i?.actions) && i?.actions.length) {
  308. return (
  309. <div
  310. key={i?.key || _index}
  311. className={styles['right-item-right-item']}
  312. >
  313. {item?.then?.length > 1 && (
  314. <div className={styles['trigger-ways']}>
  315. {i ? (i.parallel ? '并行执行' : '串行执行') : ''}
  316. </div>
  317. )}
  318. {Array.isArray(i?.actions) && (
  319. <div
  320. className={classNames(
  321. styles['right-item-right-item-contents'],
  322. )}
  323. >
  324. <div
  325. className={classNames(
  326. styles['right-item-right-item-contents-text'],
  327. )}
  328. >
  329. {branchesActionRender(i?.actions)}
  330. </div>
  331. {i?.actions.length > 3 && (
  332. <div
  333. className={classNames(
  334. styles['right-item-right-item-contents-extra'],
  335. )}
  336. >
  337. 等{i?.actions.length}个执行动作
  338. </div>
  339. )}
  340. </div>
  341. )}
  342. </div>
  343. );
  344. }
  345. return null;
  346. })}
  347. </div>
  348. ) : (
  349. ''
  350. )}
  351. </div>
  352. </div>
  353. </div>
  354. );
  355. },
  356. )}
  357. {(data?.branches || []).length > 2 && (
  358. <div
  359. className={styles['trigger-actions-more']}
  360. onClick={(e) => {
  361. e.stopPropagation();
  362. setVisible(!visible);
  363. }}
  364. >
  365. 展开更多{!visible ? <DownOutlined /> : <UpOutlined />}
  366. </div>
  367. )}
  368. </div>
  369. </div>
  370. );
  371. } else {
  372. return <div className={styles['card-item-content-box-empty']}>未配置规则</div>;
  373. }
  374. };
  375. export const ExtraSceneCard = (props: SceneCardProps) => {
  376. return (
  377. <TableCard
  378. status={props.state.value}
  379. statusText={props.state.text}
  380. statusNames={{
  381. started: StatusColorEnum.success,
  382. disable: StatusColorEnum.error,
  383. notActive: StatusColorEnum.warning,
  384. }}
  385. showTool={props.showTool}
  386. showMask={false}
  387. actions={props.tools}
  388. onClick={props.onClick}
  389. className={props.className}
  390. contentClassName={styles['content-class']}
  391. >
  392. <div className={'pro-table-card-item context-access'}>
  393. <div className={'card-item-avatar'}>
  394. <img width={88} height={88} src={imageMap.get(props.triggerType)} alt={''} />
  395. </div>
  396. <div className={'card-item-body'}>
  397. <div className={'card-item-header'}>
  398. <div className={'card-item-header-item'} style={{ maxWidth: '50%' }}>
  399. <Ellipsis
  400. title={props.name}
  401. style={{ fontSize: 18, opacity: 0.85, color: '#000', fontWeight: 'bold' }}
  402. />
  403. </div>
  404. <div className={'card-item-header-item'} style={{ maxWidth: '50%' }}>
  405. <Ellipsis
  406. title={props.description}
  407. style={{ color: 'rgba(0, 0, 0, 0.65)', margin: '3px 0 0 10px' }}
  408. />
  409. </div>
  410. </div>
  411. <ContentRender {...props} />
  412. </div>
  413. </div>
  414. <div className={styles['card-item-trigger-type']}>
  415. <div className={styles['card-item-trigger-type-text']}>
  416. <img height={16} src={iconMap.get(props.triggerType)} style={{ marginRight: 8 }} />
  417. {TriggerWayType[props.triggerType]}
  418. </div>
  419. </div>
  420. <div className={'checked-icon'}>
  421. <div>
  422. <CheckOutlined />
  423. </div>
  424. </div>
  425. </TableCard>
  426. );
  427. };
  428. export default (props: SceneCardProps) => {
  429. return (
  430. <TableCard
  431. showMask={false}
  432. detail={props.detail}
  433. showTool={props.showTool}
  434. actions={props.tools}
  435. status={props.state.value}
  436. statusText={props.state.text}
  437. statusNames={{
  438. started: StatusColorEnum.success,
  439. disable: StatusColorEnum.error,
  440. notActive: StatusColorEnum.warning,
  441. }}
  442. contentClassName={styles['content-class']}
  443. >
  444. <div className={'pro-table-card-item'} onClick={props.onClick}>
  445. <div className={'card-item-avatar'}>
  446. <img width={88} height={88} src={imageMap.get(props.triggerType)} alt={''} />
  447. </div>
  448. <div className={'card-item-body'}>
  449. <div className={'card-item-header'}>
  450. <div className={'card-item-header-item'} style={{ maxWidth: '50%' }}>
  451. <Ellipsis
  452. title={props.name}
  453. style={{ fontSize: 18, opacity: 0.85, color: '#000', fontWeight: 'bold' }}
  454. />
  455. </div>
  456. <div className={'card-item-header-item'} style={{ maxWidth: '50%' }}>
  457. <Ellipsis
  458. title={props.description}
  459. style={{ color: 'rgba(0, 0, 0, 0.65)', margin: '3px 0 0 10px' }}
  460. />
  461. </div>
  462. </div>
  463. <ContentRender {...props} />
  464. </div>
  465. </div>
  466. <div className={styles['card-item-trigger-type']}>
  467. <div className={styles['card-item-trigger-type-text']}>
  468. <img height={16} src={iconMap.get(props.triggerType)} style={{ marginRight: 8 }} />
  469. {TriggerWayType[props.triggerType]}
  470. </div>
  471. </div>
  472. </TableCard>
  473. );
  474. };