index.tsx 17 KB

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