index.tsx 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433
  1. import { PageContainer } from '@ant-design/pro-layout';
  2. import { Button, Card, Form, Input, InputNumber, Radio, Space, Switch, Tooltip } from 'antd';
  3. import { useIntl } 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, useHistory } 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. history.push(getMenuPathByCode('rule-engine/Scene'));
  120. } else {
  121. onlyMessage(resp.message);
  122. }
  123. }
  124. }, [shakeLimit]);
  125. const AntiShake = (
  126. <Space>
  127. <TitleComponent data={'触发条件'} style={{ margin: 0 }} />
  128. <Switch
  129. checked={shakeLimit.enabled}
  130. checkedChildren="开启防抖"
  131. unCheckedChildren="关闭防抖"
  132. onChange={(e) => {
  133. const newShake = {
  134. ...shakeLimit,
  135. enabled: e,
  136. };
  137. setShakeLimit(newShake);
  138. }}
  139. style={{ marginRight: 16 }}
  140. />
  141. {shakeLimit.enabled && (
  142. <>
  143. <InputNumber
  144. value={shakeLimit.time}
  145. min={1}
  146. max={100}
  147. onChange={(e: number) => {
  148. const newShake = {
  149. ...shakeLimit,
  150. time: e,
  151. };
  152. setShakeLimit(newShake);
  153. }}
  154. />
  155. <span style={{ padding: '0 16px' }}> 秒内发生 </span>
  156. <InputNumber
  157. value={shakeLimit.threshold}
  158. min={1}
  159. max={100}
  160. onChange={(e: number) => {
  161. const newShake = {
  162. ...shakeLimit,
  163. threshold: e,
  164. };
  165. setShakeLimit(newShake);
  166. }}
  167. />
  168. <span style={{ padding: '0 16px' }}>次及以上时,处理</span>
  169. <Radio.Group
  170. value={shakeLimit.alarmFirst}
  171. options={[
  172. { label: '第一次', value: true },
  173. { label: '最后一次', value: false },
  174. ]}
  175. optionType="button"
  176. onChange={(e) => {
  177. const newShake = {
  178. ...shakeLimit,
  179. alarmFirst: e.target.value,
  180. };
  181. setShakeLimit(newShake);
  182. }}
  183. ></Radio.Group>
  184. </>
  185. )}
  186. </Space>
  187. );
  188. const hasKeyInObject = (keys: string[], obj: any) => {
  189. return keys.some((key) => key in obj);
  190. };
  191. return (
  192. <PageContainer>
  193. <Card>
  194. <div className={'scene-content'}>
  195. <div className={'scene-content-left'}>
  196. <Form
  197. scrollToFirstError={{
  198. behavior: 'smooth',
  199. block: 'end',
  200. }}
  201. form={form}
  202. colon={false}
  203. name="basicForm"
  204. layout={'vertical'}
  205. preserve={false}
  206. className={'scene-save'}
  207. onValuesChange={(changeValue, allValues) => {
  208. if (changeValue.trigger) {
  209. if (changeValue.trigger.device) {
  210. if (
  211. changeValue.trigger.device.productId ||
  212. changeValue.trigger.device.selectorValues ||
  213. (changeValue.trigger.device.operation &&
  214. hasKeyInObject(
  215. ['operator', 'eventId', 'functionId'],
  216. changeValue.trigger.device.operation,
  217. ))
  218. ) {
  219. setTriggerValue([]);
  220. setRequestParams({ trigger: allValues.trigger });
  221. setTriggerDatas(allValues.trigger);
  222. }
  223. if (
  224. hasKeyInObject(['productId'], changeValue.trigger.device) ||
  225. (changeValue.trigger.device.operation &&
  226. hasKeyInObject(
  227. ['operator', 'eventId', 'functionId'],
  228. changeValue.trigger.device.operation,
  229. ))
  230. ) {
  231. setActionParams({ trigger: allValues.trigger }); // 用于内置参数请求
  232. }
  233. } else if (['timer', 'manual'].includes(changeValue.trigger.type)) {
  234. setActionParams({ trigger: allValues.trigger }); // 用于内置参数请求
  235. }
  236. }
  237. if (allValues.actions) {
  238. setActionsData(allValues.actions);
  239. }
  240. FormModel = { ...allValues };
  241. }}
  242. >
  243. <Form.Item
  244. name={'name'}
  245. label={<TitleComponent data={'名称'} style={{ margin: 0 }} />}
  246. rules={[
  247. { required: true, message: '请输入名称' },
  248. {
  249. max: 64,
  250. message: intl.formatMessage({
  251. id: 'pages.form.tip.max64',
  252. defaultMessage: '最多输入64个字符',
  253. }),
  254. },
  255. ]}
  256. >
  257. <Input placeholder={'请输入名称'} />
  258. </Form.Item>
  259. <Form.Item
  260. label={<TitleComponent data={'触发方式'} style={{ margin: 0 }} />}
  261. required
  262. >
  263. <Form.Item
  264. name={['trigger', 'type']}
  265. rules={[{ required: true, message: '请选择触发方式' }]}
  266. initialValue={'device'}
  267. >
  268. <TriggerWay onSelect={setTriggerType} disabled={isEdit} />
  269. </Form.Item>
  270. {triggerType === TriggerWayType.timing && (
  271. <TimingTrigger
  272. name={['trigger']}
  273. form={form}
  274. className={'trigger-type-content'}
  275. />
  276. )}
  277. {triggerType === TriggerWayType.device && (
  278. <TriggerDevice
  279. value={triggerDatas}
  280. className={'trigger-type-content'}
  281. form={form}
  282. />
  283. )}
  284. </Form.Item>
  285. {triggerType === TriggerWayType.device &&
  286. requestParams &&
  287. requestParams.trigger?.device?.productId ? (
  288. <Form.Item label={AntiShake}>
  289. <TriggerTerm ref={triggerRef} params={requestParams} value={triggerValue} />
  290. </Form.Item>
  291. ) : null}
  292. <Form.Item hidden name={'parallel'} initialValue={true}>
  293. <Input />
  294. </Form.Item>
  295. <Form.Item
  296. label={
  297. <Space>
  298. <TitleComponent data={<span>执行动作</span>} style={{ margin: 0 }} />
  299. <Tooltip
  300. title={
  301. <div>
  302. <div>串行:按顺序依次执行动作</div>
  303. <div>并行:同时执行所有动作</div>
  304. </div>
  305. }
  306. >
  307. <QuestionCircleOutlined style={{ margin: '0 8px', fontSize: 14 }} />
  308. </Tooltip>
  309. <Radio.Group
  310. value={parallel}
  311. options={[
  312. { label: '串行', value: false },
  313. { label: '并行', value: true },
  314. ]}
  315. optionType="button"
  316. onChange={(e) => {
  317. setParallel(e.target.value);
  318. form.setFieldsValue({ parallel: e.target.value });
  319. }}
  320. ></Radio.Group>
  321. </Space>
  322. }
  323. >
  324. <Form.List name="actions">
  325. {(fields, { add, remove, move }, { errors }) => (
  326. <>
  327. <div className={'scene-actions'} style={{ paddingBottom: 24 }}>
  328. {fields.map(({ key, name, ...restField }) => (
  329. <ActionItems
  330. key={key}
  331. form={form}
  332. restField={restField}
  333. name={name}
  334. trigger={actionParams}
  335. triggerType={triggerType}
  336. triggerRef={triggerRef.current}
  337. onRemove={() => {
  338. remove(name);
  339. setActionDataCount(actionDataCount - 1);
  340. }}
  341. onMove={(type) => {
  342. if (type === 'up') {
  343. move(name, name - 1);
  344. } else {
  345. move(name, name + 1);
  346. }
  347. }}
  348. actionItemData={actionsData[name]}
  349. isLast={!actionDataCount || actionDataCount - 1 === name}
  350. parallel={parallel}
  351. isEdit={isEdit}
  352. />
  353. ))}
  354. <Form.Item noStyle>
  355. <Button
  356. type="primary"
  357. ghost
  358. style={{
  359. width: '100%',
  360. marginTop: 16,
  361. }}
  362. onClick={() => {
  363. add();
  364. setActionDataCount(actionDataCount + 1);
  365. }}
  366. icon={<PlusOutlined />}
  367. >
  368. 新增
  369. </Button>
  370. </Form.Item>
  371. </div>
  372. <Form.ErrorList errors={errors} />
  373. </>
  374. )}
  375. </Form.List>
  376. </Form.Item>
  377. <Form.Item
  378. label={<TitleComponent data={'说明'} style={{ margin: 0 }} />}
  379. name={'description'}
  380. >
  381. <Input.TextArea showCount maxLength={200} placeholder={'请输入说明'} rows={4} />
  382. </Form.Item>
  383. {/*{triggerType === TriggerWayType.device &&*/}
  384. {/*requestParams &&*/}
  385. {/*requestParams.trigger?.device?.productId ? (*/}
  386. {/* <Form.Item hidden name={['trigger','shakeLimit']}>*/}
  387. {/* <Input />*/}
  388. {/* </Form.Item>*/}
  389. {/*) : null}*/}
  390. <Form.Item hidden name={'id'}>
  391. <Input />
  392. </Form.Item>
  393. {!view && (
  394. <PermissionButton
  395. isPermission={getOtherPermission(['add', 'update'])}
  396. onClick={saveData}
  397. type={'primary'}
  398. loading={loading}
  399. htmlType="submit"
  400. >
  401. 保存
  402. </PermissionButton>
  403. )}
  404. </Form>
  405. </div>
  406. <div className={'scene-content-right'}>
  407. <Explanation type={triggerType} />
  408. </div>
  409. </div>
  410. </Card>
  411. </PageContainer>
  412. );
  413. };