index.tsx 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562
  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. const imageMap = new Map();
  13. imageMap.set('timer', require('/public/images/scene/scene-timer.png'));
  14. imageMap.set('manual', require('/public/images/scene/scene-hand.png'));
  15. imageMap.set('device', require('/public/images/scene/scene-device.png'));
  16. const iconMap = new Map();
  17. iconMap.set('timer', require('/public/images/scene/trigger-type-icon/timing.png'));
  18. iconMap.set('manual', require('/public/images/scene/trigger-type-icon/manual.png'));
  19. iconMap.set('device', require('/public/images/scene/trigger-type-icon/device.png'));
  20. // @ts-ignore
  21. export interface SceneCardProps extends SceneItem {
  22. detail?: React.ReactNode;
  23. tools?: React.ReactNode[];
  24. avatarSize?: number;
  25. className?: string;
  26. onUnBind?: (e: any) => void;
  27. showBindBtn?: boolean;
  28. cardType?: 'bind' | 'unbind';
  29. showTool?: boolean;
  30. onClick?: () => void;
  31. }
  32. enum TriggerWayType {
  33. manual = '手动触发',
  34. timer = '定时触发',
  35. device = '设备触发',
  36. }
  37. enum UnitEnum {
  38. seconds = '秒',
  39. minutes = '分',
  40. hours = '小时',
  41. }
  42. // const selectorRender = (obj: any) => {
  43. // switch (obj?.selector) {
  44. // case 'all':
  45. // return (
  46. // <span>
  47. // 所有的<span className={styles['trigger-device']}>{obj?.productId}</span>
  48. // </span>
  49. // );
  50. // case 'fixed':
  51. // return (
  52. // <span>
  53. // 设备
  54. // <span className={styles['trigger-device']}>
  55. // {(obj?.selectorValues || '').map((item: any) => item?.name).join(',')}
  56. // </span>
  57. // </span>
  58. // );
  59. // case 'org':
  60. // return (
  61. // <span>
  62. // 部门
  63. // <span className={styles['trigger-device']}>
  64. // {(obj?.selectorValues || '').map((item: any) => item?.name).join(',')}
  65. // </span>
  66. // </span>
  67. // );
  68. // default:
  69. // return '';
  70. // }
  71. // };
  72. // const selectorContextRender = (obj: any) => {
  73. // switch (obj?.selector) {
  74. // case 'all':
  75. // return `所有的${obj?.productId}`;
  76. // case 'fixed':
  77. // return `设备${(obj?.selectorValues || '').map((item: any) => item?.name).join(',')}`;
  78. // case 'org':
  79. // return `部门${(obj?.selectorValues || '').map((item: any) => item?.name).join(',')}`;
  80. // default:
  81. // return '';
  82. // }
  83. // };
  84. // const timerRender = (timer: any) => {
  85. // if (timer?.trigger && timer?.mod) {
  86. // const trigger = timer?.trigger;
  87. // const mod = timer?.mod;
  88. // const str: string = trigger === 'week' ? '星期' : trigger === 'month' ? '月' : timer?.cron;
  89. // if (mod === 'once') {
  90. // return `,每${str}${timer.when.join('/')},${timer?.once.time}执行一次`;
  91. // } else {
  92. // return `每${str}${timer.when.join('/')},${timer?.period?.from}-${timer?.period.to},每${
  93. // timer?.period.every
  94. // }${UnitEnum[timer?.period?.unit]}执行一次`;
  95. // }
  96. // }
  97. // return '';
  98. // };
  99. // const operatorRender = (operation: any) => {
  100. // switch (operation?.operator) {
  101. // case 'online':
  102. // return '上线';
  103. // case 'offline':
  104. // return '离线';
  105. // case 'reportEvent':
  106. // return `上报事件${operation?.options?.eventName}`;
  107. // case 'reportProperty':
  108. // return `上报属性${(operation?.options?.propertiesName || []).join(',')}`;
  109. // case 'readProperty':
  110. // return `读取属性${(operation?.options?.propertiesName || []).join(',')}`;
  111. // case 'writeProperty':
  112. // return `修改属性${(operation?.options?.propertiesName || []).join(',')}`;
  113. // case 'invokeFunction':
  114. // return `调用功能${operation?.options?.functionName}`;
  115. // default:
  116. // return '';
  117. // }
  118. // };
  119. const notifyRender = (data: ActionsType | undefined) => {
  120. switch (data?.notify?.notifyType) {
  121. case 'dingTalk':
  122. return (
  123. <div className={styles['notify-img-highlight']}>
  124. 向<span>{data?.options?.notifierName || data?.notify?.notifierId}</span>
  125. 通过<span>钉钉</span>发送
  126. <span>{data?.options?.templateName || data?.notify?.templateId}</span>
  127. </div>
  128. );
  129. case 'weixin':
  130. return (
  131. <div className={styles['notify-img-highlight']}>
  132. 向<span>{data?.options?.sendTo || ''}</span>
  133. <span>{data?.options?.orgName || ''}</span>
  134. <span>{data?.options?.tagName || ''}</span>
  135. 通过<span>微信</span>发送
  136. <span>{data?.options?.templateName || data?.notify?.templateId}</span>
  137. </div>
  138. );
  139. case 'email':
  140. return (
  141. <div className={styles['notify-img-highlight']}>
  142. 向<span>{data?.options?.sendTo || ''}</span>
  143. 通过<span>邮件</span>发送
  144. <span>{data?.options?.templateName || data?.notify?.templateId}</span>
  145. </div>
  146. );
  147. case 'voice':
  148. return (
  149. <div className={styles['notify-img-highlight']}>
  150. 向<span>{data?.options?.calledNumber || ''}</span>
  151. 通过<span>语音</span>发送
  152. <span>{data?.options?.templateName || data?.notify?.templateId}</span>
  153. </div>
  154. );
  155. case 'sms':
  156. return (
  157. <div className={styles['notify-img-highlight']}>
  158. 向{data?.options?.sendTo || ''}通过短信发送
  159. {data?.options?.templateName || data?.notify?.templateId}
  160. </div>
  161. );
  162. case 'webhook':
  163. return (
  164. <div className={styles['notify-img-highlight']}>
  165. 通过<span>webhook</span>发送
  166. <span>{data?.options?.templateName || data?.notify?.templateId}</span>
  167. </div>
  168. );
  169. default:
  170. return null;
  171. }
  172. };
  173. const deviceRender = (data: ActionsType | undefined) => {
  174. switch (data?.device?.selector) {
  175. case 'fixed':
  176. return (
  177. <div className={styles['notify-text-highlight']}>
  178. {data?.options?.type}
  179. <span className={styles['trigger-device']}>{data?.options?.name}</span>
  180. {data?.options?.properties}
  181. </div>
  182. );
  183. case 'tag':
  184. return (
  185. <div className={styles['notify-text-highlight']}>
  186. {data?.options?.type}
  187. {data.options?.taglist.map((item: any) => (
  188. <span className={styles['trigger-device']}>
  189. {item.type}
  190. {item.name}
  191. {item.value}
  192. </span>
  193. ))}
  194. {data?.options?.productName}
  195. {data?.options?.properties}
  196. </div>
  197. );
  198. case 'relation':
  199. return (
  200. <div className={styles['notify-text-highlight']}>
  201. {data?.options?.type}与
  202. <span className={styles['trigger-device']}>{data?.options?.name}</span>具有相同
  203. {data?.options?.relationName}的{data?.options?.productName}设备的
  204. {data?.options?.properties}
  205. </div>
  206. );
  207. default:
  208. return null;
  209. }
  210. };
  211. const actionRender = (action: ActionsType, index: number) => {
  212. switch (action?.executor) {
  213. case 'notify':
  214. return (
  215. <div
  216. className={styles['card-item-content-action-item-right-item']}
  217. key={action?.key || index}
  218. >
  219. <div className={classNames(styles['trigger-contents'], 'ellipsis')}>
  220. {notifyRender(action)}
  221. </div>
  222. </div>
  223. );
  224. case 'delay':
  225. return (
  226. <div
  227. className={styles['card-item-content-action-item-right-item']}
  228. key={action?.key || index}
  229. >
  230. <div className={classNames(styles['trigger-contents'], 'ellipsis')}>
  231. <span style={{ fontWeight: 'bold' }}>
  232. {action?.delay?.time}
  233. {UnitEnum[action?.delay?.unit || '']}
  234. </span>
  235. 后执行后续动作
  236. </div>
  237. </div>
  238. );
  239. case 'device':
  240. return (
  241. <div
  242. className={styles['card-item-content-action-item-right-item']}
  243. key={action?.key || index}
  244. >
  245. <div className={classNames(styles['trigger-contents'], 'ellipsis')}>
  246. {deviceRender(action)}
  247. </div>
  248. </div>
  249. );
  250. case 'alarm':
  251. return (
  252. <div
  253. className={styles['card-item-content-action-item-right-item']}
  254. key={action?.key || index}
  255. >
  256. <div className={classNames(styles['trigger-contents'], 'ellipsis')}>
  257. 满足条件后将触发关联此场景的告警
  258. </div>
  259. </div>
  260. );
  261. default:
  262. return null;
  263. }
  264. };
  265. const conditionsRender = (when: any[], index: number) => {
  266. if (when.length && when[index]) {
  267. let str: string = '';
  268. (when[index]?.terms || []).map((i: any, _index: number) => {
  269. str += `${i?.terms[0] || ''}${
  270. i?.termType && when[index]?.terms.length !== _index + 1 ? i?.termType : ''
  271. }`;
  272. });
  273. return str;
  274. }
  275. return '';
  276. };
  277. const branchesActionRender = (actions: any[]) => {
  278. if (actions && actions?.length) {
  279. const list: any[] = [];
  280. actions.map((item, index) => {
  281. const dt = actionRender(item, index);
  282. list.push(dt);
  283. });
  284. return list.map((item, index) => (
  285. <div className={styles['right-item-right-item-contents-item']}>
  286. <div style={{ margin: '0 10px' }}>{item}</div>
  287. <MyTooltip title={actions[index]?.options?.terms || ''}>
  288. {actions[index]?.options?.terms && (
  289. <div className={'ellipsis'} style={{ minWidth: 40 }}>
  290. 动作{index + 1}
  291. {actions[index]?.options?.terms}
  292. </div>
  293. )}
  294. </MyTooltip>
  295. </div>
  296. ));
  297. }
  298. return '';
  299. };
  300. const ContentRender = (data: SceneCardProps) => {
  301. const [visible, setVisible] = useState<boolean>(false);
  302. const type = data.triggerType;
  303. if (!!type && (data.branches || [])?.length) {
  304. const trigger = data?.options?.trigger;
  305. return (
  306. <div className={styles['card-item-content-box']}>
  307. <div className={classNames(styles['card-item-content-trigger'])}>
  308. {trigger?.name && (
  309. <MyTooltip placement="topLeft" title={trigger?.name || ''}>
  310. <div
  311. className={classNames(styles['card-item-content-trigger-item'], 'ellipsis')}
  312. style={{ maxWidth: '15%', color: 'rgba(47, 84, 235)' }}
  313. >
  314. {trigger?.name || ''}
  315. </div>
  316. </MyTooltip>
  317. )}
  318. {trigger?.productName && (
  319. <MyTooltip placement="topLeft" title={trigger?.productName || ''}>
  320. <div
  321. className={classNames(styles['card-item-content-trigger-item'], 'ellipsis')}
  322. style={{ maxWidth: '15%', color: 'rgba(47, 84, 235)' }}
  323. >
  324. {trigger?.productName || ''}
  325. </div>
  326. </MyTooltip>
  327. )}
  328. {trigger?.when && (
  329. <MyTooltip placement="topLeft" title={trigger?.when || ''}>
  330. <div
  331. className={classNames(styles['card-item-content-trigger-item'], 'ellipsis')}
  332. style={{ maxWidth: '15%' }}
  333. >
  334. {trigger?.when || ''}
  335. </div>
  336. </MyTooltip>
  337. )}
  338. {trigger?.time && (
  339. <div className={classNames(styles['card-item-content-trigger-item'])}>
  340. {trigger?.time || ''}
  341. </div>
  342. )}
  343. {trigger?.extraTime && (
  344. <div className={classNames(styles['card-item-content-trigger-item'])}>
  345. {trigger?.extraTime || ''}
  346. </div>
  347. )}
  348. {trigger?.action && (
  349. <MyTooltip placement="topLeft" title={trigger?.action || ''}>
  350. <div
  351. className={classNames(styles['card-item-content-trigger-item'], 'ellipsis')}
  352. style={{ maxWidth: '15%' }}
  353. >
  354. {trigger?.action || ''}
  355. </div>
  356. </MyTooltip>
  357. )}
  358. {trigger?.type && (
  359. <div className={classNames(styles['card-item-content-trigger-item'])}>
  360. {trigger?.type || ''}
  361. </div>
  362. )}
  363. </div>
  364. <div className={styles['card-item-content-action']}>
  365. {(visible ? data.branches || [] : (data?.branches || []).slice(0, 2)).map(
  366. (item: any, index) => {
  367. return (
  368. <div className={styles['card-item-content-action-item']} key={item?.key || index}>
  369. <div className={styles['card-item-content-action-item-left']}>
  370. {type === 'device' ? (index === 0 ? '当' : '否则') : '执行'}
  371. </div>
  372. <div className={styles['card-item-content-action-item-right']}>
  373. <div className={styles['card-item-content-action-item-right-item']}>
  374. {type === 'device' && (
  375. <div
  376. className={styles['right-item-left']}
  377. style={{
  378. width: Array.isArray(item.then) && item?.then.length ? '15%' : '100%',
  379. }}
  380. >
  381. <MyTooltip
  382. placement={'topLeft'}
  383. title={conditionsRender(data.options?.when || [], index)}
  384. >
  385. <div className={classNames(styles['trigger-conditions'], 'ellipsis')}>
  386. {conditionsRender(data.options?.when || [], index)}
  387. </div>
  388. </MyTooltip>
  389. {item.shakeLimit?.enabled && (
  390. <MyTooltip
  391. title={`(${item.shakeLimit?.time}秒内发生${item.shakeLimit?.threshold}
  392. 次以上时执行一次)`}
  393. >
  394. <div className={classNames(styles['trigger-shake'], 'ellipsis')}>
  395. ({item.shakeLimit?.time}秒内发生{item.shakeLimit?.threshold}
  396. 次以上时执行一次)
  397. </div>
  398. </MyTooltip>
  399. )}
  400. </div>
  401. )}
  402. {Array.isArray(item.then) && item?.then.length ? (
  403. <div
  404. className={styles['right-item-right']}
  405. style={{ width: type === 'device' ? '85%' : '100%' }}
  406. >
  407. {(item?.then || []).map((i: BranchesThen, _index: number) => (
  408. <div key={i?.key || _index} className={styles['right-item-right-item']}>
  409. <div className={styles['trigger-ways']}>
  410. {i.parallel ? '并行执行' : '串行执行'}
  411. </div>
  412. <div className={classNames(styles['right-item-right-item-contents'])}>
  413. {branchesActionRender(Array.isArray(i?.actions) ? i?.actions : [])}
  414. </div>
  415. </div>
  416. ))}
  417. </div>
  418. ) : (
  419. ''
  420. )}
  421. </div>
  422. </div>
  423. </div>
  424. );
  425. },
  426. )}
  427. {(data?.branches || []).length > 2 && (
  428. <div
  429. className={styles['trigger-actions-more']}
  430. onClick={(e) => {
  431. e.stopPropagation();
  432. setVisible(!visible);
  433. }}
  434. >
  435. 展开更多{!visible ? <DownOutlined /> : <UpOutlined />}
  436. </div>
  437. )}
  438. </div>
  439. </div>
  440. );
  441. } else {
  442. return <div className={styles['card-item-content-box-empty']}>未配置规则</div>;
  443. }
  444. };
  445. export const ExtraSceneCard = (props: SceneCardProps) => {
  446. return (
  447. <TableCard
  448. status={props.state.value}
  449. statusText={props.state.text}
  450. statusNames={{
  451. started: StatusColorEnum.success,
  452. disable: StatusColorEnum.error,
  453. notActive: StatusColorEnum.warning,
  454. }}
  455. showTool={props.showTool}
  456. showMask={false}
  457. actions={props.tools}
  458. onClick={props.onClick}
  459. className={props.className}
  460. contentClassName={styles['content-class']}
  461. >
  462. <div className={'pro-table-card-item context-access'}>
  463. <div className={'card-item-avatar'}>
  464. <img width={88} height={88} src={imageMap.get(props.triggerType)} alt={''} />
  465. </div>
  466. <div className={'card-item-body'}>
  467. <div className={'card-item-header'}>
  468. <div className={'card-item-header-item'} style={{ maxWidth: '50%' }}>
  469. <Ellipsis
  470. title={props.name}
  471. style={{ fontSize: 18, opacity: 0.85, color: '#000', fontWeight: 'bold' }}
  472. />
  473. </div>
  474. <div className={'card-item-header-item'} style={{ maxWidth: '50%' }}>
  475. <Ellipsis
  476. title={props.description}
  477. style={{ color: 'rgba(0, 0, 0, 0.65)', margin: '3px 0 0 10px' }}
  478. />
  479. </div>
  480. </div>
  481. <ContentRender {...props} />
  482. </div>
  483. </div>
  484. <div className={styles['card-item-trigger-type']}>
  485. <div className={styles['card-item-trigger-type-text']}>
  486. <img height={16} src={iconMap.get(props.triggerType)} style={{ marginRight: 8 }} />
  487. {TriggerWayType[props.triggerType]}
  488. </div>
  489. </div>
  490. <div className={'checked-icon'}>
  491. <div>
  492. <CheckOutlined />
  493. </div>
  494. </div>
  495. </TableCard>
  496. );
  497. };
  498. export default (props: SceneCardProps) => {
  499. return (
  500. <TableCard
  501. showMask={false}
  502. detail={props.detail}
  503. showTool={props.showTool}
  504. actions={props.tools}
  505. status={props.state.value}
  506. statusText={props.state.text}
  507. statusNames={{
  508. started: StatusColorEnum.success,
  509. disable: StatusColorEnum.error,
  510. notActive: StatusColorEnum.warning,
  511. }}
  512. contentClassName={styles['content-class']}
  513. >
  514. <div className={'pro-table-card-item'} onClick={props.onClick}>
  515. <div className={'card-item-avatar'}>
  516. <img width={88} height={88} src={imageMap.get(props.triggerType)} alt={''} />
  517. </div>
  518. <div className={'card-item-body'}>
  519. <div className={'card-item-header'}>
  520. <div className={'card-item-header-item'} style={{ maxWidth: '50%' }}>
  521. <Ellipsis
  522. title={props.name}
  523. style={{ fontSize: 18, opacity: 0.85, color: '#000', fontWeight: 'bold' }}
  524. />
  525. </div>
  526. <div className={'card-item-header-item'} style={{ maxWidth: '50%' }}>
  527. <Ellipsis
  528. title={props.description}
  529. style={{ color: 'rgba(0, 0, 0, 0.65)', margin: '3px 0 0 10px' }}
  530. />
  531. </div>
  532. </div>
  533. <ContentRender {...props} />
  534. </div>
  535. </div>
  536. <div className={styles['card-item-trigger-type']}>
  537. <div className={styles['card-item-trigger-type-text']}>
  538. <img height={16} src={iconMap.get(props.triggerType)} style={{ marginRight: 8 }} />
  539. {TriggerWayType[props.triggerType]}
  540. </div>
  541. </div>
  542. </TableCard>
  543. );
  544. };