index.tsx 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434
  1. import { PageContainer } from '@ant-design/pro-layout';
  2. import { Button, Card, Form, Input, InputNumber, Radio, Space, Switch, Tooltip } from 'antd';
  3. import { useIntl, useHistory } from 'umi';
  4. import { useCallback, useEffect, useRef, useState } from 'react';
  5. import { PermissionButton, TitleComponent } from '@/components';
  6. import ActionItems from './action/action';
  7. import { PlusOutlined, QuestionCircleOutlined } from '@ant-design/icons';
  8. import { TimingTrigger, TriggerWay } from './components';
  9. import { TriggerWayType } from './components/TriggerWay';
  10. import TriggerTerm from '@/pages/rule-engine/Scene/TriggerTerm';
  11. import TriggerDevice from './trigger';
  12. import { service } from '../index';
  13. import './index.less';
  14. import { model } from '@formily/reactive';
  15. import type { FormModelType } from '@/pages/rule-engine/Scene/typings';
  16. import { onlyMessage } from '@/utils/util';
  17. import Explanation from './Explanation';
  18. import { getMenuPathByCode } from '@/utils/menu';
  19. import { useLocation } from '@/hooks';
  20. type ShakeLimitType = {
  21. enabled: boolean;
  22. groupType?: string;
  23. time?: number;
  24. threshold?: number;
  25. alarmFirst?: boolean;
  26. };
  27. const DefaultShakeLimit = {
  28. enabled: false,
  29. alarmFirst: false,
  30. };
  31. export let FormModel = model<FormModelType>({});
  32. export default () => {
  33. const location = useLocation();
  34. const [form] = Form.useForm();
  35. const intl = useIntl();
  36. const triggerRef = useRef<any>();
  37. const history = useHistory();
  38. const { getOtherPermission } = PermissionButton.usePermission('rule-engine/Scene');
  39. const [triggerType, setTriggerType] = useState('device');
  40. const [loading, setLoading] = useState(false);
  41. const [parallel, setParallel] = useState(true); // 是否并行
  42. const [shakeLimit, setShakeLimit] = useState<ShakeLimitType>(DefaultShakeLimit);
  43. const [requestParams, setRequestParams] = useState<any>(undefined);
  44. const [triggerValue, setTriggerValue] = useState<any>([]);
  45. const [triggerDatas, setTriggerDatas] = useState<any>({});
  46. const [actionParams, setActionParams] = useState<any>(undefined);
  47. const [actionDataCount, setActionDataCount] = useState(0);
  48. const [actionsData, setActionsData] = useState<any[]>([]);
  49. const [isEdit, setIsEdit] = useState(false);
  50. const [view, setView] = useState<boolean>(false);
  51. useEffect(() => {
  52. FormModel = {};
  53. }, []);
  54. const getDetail = useCallback(
  55. async (id: string) => {
  56. const resp = await service.detail(id);
  57. if (resp.status === 200 && resp.result) {
  58. setIsEdit(true);
  59. const _data: any = resp.result;
  60. FormModel = _data;
  61. form.setFieldsValue(_data);
  62. setParallel(_data.parallel);
  63. setTriggerValue({ trigger: _data.terms || [] });
  64. setTriggerDatas(_data.trigger);
  65. setActionParams({ trigger: _data.trigger });
  66. if (_data.trigger?.shakeLimit) {
  67. setShakeLimit(_data.trigger?.shakeLimit || DefaultShakeLimit);
  68. }
  69. if (_data.trigger?.device) {
  70. setRequestParams({ trigger: _data.trigger });
  71. }
  72. if (_data.actions) {
  73. setActionsData(_data.actions);
  74. }
  75. }
  76. },
  77. [triggerRef],
  78. );
  79. useEffect(() => {
  80. const params = new URLSearchParams(location.search);
  81. const id = params.get('id');
  82. if (id) {
  83. getDetail(id);
  84. }
  85. if (location && location.state) {
  86. setView(location.state.view);
  87. }
  88. }, [location]);
  89. const saveData = useCallback(async () => {
  90. const formData = await form.validateFields();
  91. let triggerData = undefined;
  92. // 获取触发条件数据
  93. if (triggerRef.current && formData.trigger) {
  94. triggerData = await triggerRef.current.getTriggerForm();
  95. if (triggerData) {
  96. formData.terms = triggerData.trigger;
  97. }
  98. }
  99. console.log('save', formData);
  100. if (formData) {
  101. if (shakeLimit.enabled) {
  102. formData.trigger = {
  103. ...formData.trigger,
  104. shakeLimit: shakeLimit,
  105. };
  106. }
  107. setLoading(true);
  108. const resp = formData.id ? await service.updateScene(formData) : await service.save(formData);
  109. // 处理跳转新增
  110. if ((window as any).onTabSaveSuccess) {
  111. if (resp.result) {
  112. (window as any).onTabSaveSuccess(resp);
  113. setTimeout(() => window.close(), 300);
  114. }
  115. }
  116. setLoading(false);
  117. if (resp.status === 200) {
  118. onlyMessage('操作成功');
  119. const url = getMenuPathByCode('rule-engine/Scene');
  120. history.push(url);
  121. } else {
  122. onlyMessage(resp.message);
  123. }
  124. }
  125. }, [shakeLimit]);
  126. const AntiShake = (
  127. <Space>
  128. <TitleComponent data={'触发条件'} style={{ margin: 0 }} />
  129. <Switch
  130. checked={shakeLimit.enabled}
  131. checkedChildren="开启防抖"
  132. unCheckedChildren="关闭防抖"
  133. onChange={(e) => {
  134. const newShake = {
  135. ...shakeLimit,
  136. enabled: e,
  137. };
  138. setShakeLimit(newShake);
  139. }}
  140. style={{ marginRight: 16 }}
  141. />
  142. {shakeLimit.enabled && (
  143. <>
  144. <InputNumber
  145. value={shakeLimit.time}
  146. min={1}
  147. max={100}
  148. onChange={(e: number) => {
  149. const newShake = {
  150. ...shakeLimit,
  151. time: e,
  152. };
  153. setShakeLimit(newShake);
  154. }}
  155. />
  156. <span style={{ padding: '0 16px' }}> 秒内发生 </span>
  157. <InputNumber
  158. value={shakeLimit.threshold}
  159. min={1}
  160. max={100}
  161. onChange={(e: number) => {
  162. const newShake = {
  163. ...shakeLimit,
  164. threshold: e,
  165. };
  166. setShakeLimit(newShake);
  167. }}
  168. />
  169. <span style={{ padding: '0 16px' }}>次及以上时,处理</span>
  170. <Radio.Group
  171. value={shakeLimit.alarmFirst}
  172. options={[
  173. { label: '第一次', value: true },
  174. { label: '最后一次', value: false },
  175. ]}
  176. optionType="button"
  177. onChange={(e) => {
  178. const newShake = {
  179. ...shakeLimit,
  180. alarmFirst: e.target.value,
  181. };
  182. setShakeLimit(newShake);
  183. }}
  184. ></Radio.Group>
  185. </>
  186. )}
  187. </Space>
  188. );
  189. const hasKeyInObject = (keys: string[], obj: any) => {
  190. return keys.some((key) => key in obj);
  191. };
  192. return (
  193. <PageContainer>
  194. <Card>
  195. <div className={'scene-content'}>
  196. <div className={'scene-content-left'}>
  197. <Form
  198. scrollToFirstError={{
  199. behavior: 'smooth',
  200. block: 'end',
  201. }}
  202. form={form}
  203. colon={false}
  204. name="basicForm"
  205. layout={'vertical'}
  206. preserve={false}
  207. className={'scene-save'}
  208. onValuesChange={(changeValue, allValues) => {
  209. if (changeValue.trigger) {
  210. if (changeValue.trigger.device) {
  211. if (
  212. changeValue.trigger.device.productId ||
  213. changeValue.trigger.device.selectorValues ||
  214. (changeValue.trigger.device.operation &&
  215. hasKeyInObject(
  216. ['operator', 'eventId', 'functionId'],
  217. changeValue.trigger.device.operation,
  218. ))
  219. ) {
  220. setTriggerValue([]);
  221. setRequestParams({ trigger: allValues.trigger });
  222. setTriggerDatas(allValues.trigger);
  223. }
  224. if (
  225. hasKeyInObject(['productId'], changeValue.trigger.device) ||
  226. (changeValue.trigger.device.operation &&
  227. hasKeyInObject(
  228. ['operator', 'eventId', 'functionId'],
  229. changeValue.trigger.device.operation,
  230. ))
  231. ) {
  232. setActionParams({ trigger: allValues.trigger }); // 用于内置参数请求
  233. }
  234. } else if (['timer', 'manual'].includes(changeValue.trigger.type)) {
  235. setActionParams({ trigger: allValues.trigger }); // 用于内置参数请求
  236. }
  237. }
  238. if (allValues.actions) {
  239. setActionsData(allValues.actions);
  240. }
  241. FormModel = { ...allValues };
  242. }}
  243. >
  244. <Form.Item
  245. name={'name'}
  246. label={<TitleComponent data={'名称'} style={{ margin: 0 }} />}
  247. rules={[
  248. { required: true, message: '请输入名称' },
  249. {
  250. max: 64,
  251. message: intl.formatMessage({
  252. id: 'pages.form.tip.max64',
  253. defaultMessage: '最多输入64个字符',
  254. }),
  255. },
  256. ]}
  257. >
  258. <Input placeholder={'请输入名称'} />
  259. </Form.Item>
  260. <Form.Item
  261. label={<TitleComponent data={'触发方式'} style={{ margin: 0 }} />}
  262. required
  263. >
  264. <Form.Item
  265. name={['trigger', 'type']}
  266. rules={[{ required: true, message: '请选择触发方式' }]}
  267. initialValue={'device'}
  268. >
  269. <TriggerWay onSelect={setTriggerType} disabled={isEdit} />
  270. </Form.Item>
  271. {triggerType === TriggerWayType.timing && (
  272. <TimingTrigger
  273. name={['trigger']}
  274. form={form}
  275. className={'trigger-type-content'}
  276. />
  277. )}
  278. {triggerType === TriggerWayType.device && (
  279. <TriggerDevice
  280. value={triggerDatas}
  281. className={'trigger-type-content'}
  282. form={form}
  283. />
  284. )}
  285. </Form.Item>
  286. {triggerType === TriggerWayType.device &&
  287. requestParams &&
  288. requestParams.trigger?.device?.productId ? (
  289. <Form.Item label={AntiShake}>
  290. <TriggerTerm ref={triggerRef} params={requestParams} value={triggerValue} />
  291. </Form.Item>
  292. ) : null}
  293. <Form.Item hidden name={'parallel'} initialValue={true}>
  294. <Input />
  295. </Form.Item>
  296. <Form.Item
  297. label={
  298. <Space>
  299. <TitleComponent data={<span>执行动作</span>} style={{ margin: 0 }} />
  300. <Tooltip
  301. title={
  302. <div>
  303. <div>串行:按顺序依次执行动作</div>
  304. <div>并行:同时执行所有动作</div>
  305. </div>
  306. }
  307. >
  308. <QuestionCircleOutlined style={{ margin: '0 8px', fontSize: 14 }} />
  309. </Tooltip>
  310. <Radio.Group
  311. value={parallel}
  312. options={[
  313. { label: '串行', value: false },
  314. { label: '并行', value: true },
  315. ]}
  316. optionType="button"
  317. onChange={(e) => {
  318. setParallel(e.target.value);
  319. form.setFieldsValue({ parallel: e.target.value });
  320. }}
  321. ></Radio.Group>
  322. </Space>
  323. }
  324. >
  325. <Form.List name="actions">
  326. {(fields, { add, remove, move }, { errors }) => (
  327. <>
  328. <div className={'scene-actions'} style={{ paddingBottom: 24 }}>
  329. {fields.map(({ key, name, ...restField }) => (
  330. <ActionItems
  331. key={key}
  332. form={form}
  333. restField={restField}
  334. name={name}
  335. trigger={actionParams}
  336. triggerType={triggerType}
  337. triggerRef={triggerRef.current}
  338. onRemove={() => {
  339. remove(name);
  340. setActionDataCount(actionDataCount - 1);
  341. }}
  342. onMove={(type) => {
  343. if (type === 'up') {
  344. move(name, name - 1);
  345. } else {
  346. move(name, name + 1);
  347. }
  348. }}
  349. actionItemData={actionsData[name]}
  350. isLast={!actionDataCount || actionDataCount - 1 === name}
  351. parallel={parallel}
  352. isEdit={isEdit}
  353. />
  354. ))}
  355. <Form.Item noStyle>
  356. <Button
  357. type="primary"
  358. ghost
  359. style={{
  360. width: '100%',
  361. marginTop: 16,
  362. }}
  363. onClick={() => {
  364. add();
  365. setActionDataCount(actionDataCount + 1);
  366. }}
  367. icon={<PlusOutlined />}
  368. >
  369. 新增
  370. </Button>
  371. </Form.Item>
  372. </div>
  373. <Form.ErrorList errors={errors} />
  374. </>
  375. )}
  376. </Form.List>
  377. </Form.Item>
  378. <Form.Item
  379. label={<TitleComponent data={'说明'} style={{ margin: 0 }} />}
  380. name={'description'}
  381. >
  382. <Input.TextArea showCount maxLength={200} placeholder={'请输入说明'} rows={4} />
  383. </Form.Item>
  384. {/*{triggerType === TriggerWayType.device &&*/}
  385. {/*requestParams &&*/}
  386. {/*requestParams.trigger?.device?.productId ? (*/}
  387. {/* <Form.Item hidden name={['trigger','shakeLimit']}>*/}
  388. {/* <Input />*/}
  389. {/* </Form.Item>*/}
  390. {/*) : null}*/}
  391. <Form.Item hidden name={'id'}>
  392. <Input />
  393. </Form.Item>
  394. {!view && (
  395. <PermissionButton
  396. isPermission={getOtherPermission(['add', 'update'])}
  397. onClick={saveData}
  398. type={'primary'}
  399. loading={loading}
  400. htmlType="submit"
  401. >
  402. 保存
  403. </PermissionButton>
  404. )}
  405. </Form>
  406. </div>
  407. <div className={'scene-content-right'}>
  408. <Explanation type={triggerType} />
  409. </div>
  410. </div>
  411. </Card>
  412. </PageContainer>
  413. );
  414. };