index.tsx 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724
  1. import { PageContainer } from '@ant-design/pro-layout';
  2. import type { ActionType, ProColumns } from '@jetlinks/pro-table';
  3. import type { DeviceInstance } from '@/pages/device/Instance/typings';
  4. import moment from 'moment';
  5. import { Badge, Button, Dropdown, Menu, message, Tooltip } from 'antd';
  6. import { useEffect, useRef, useState } from 'react';
  7. import { useHistory, useIntl } from 'umi';
  8. import {
  9. CheckCircleOutlined,
  10. DeleteOutlined,
  11. EditOutlined,
  12. ExportOutlined,
  13. EyeOutlined,
  14. ImportOutlined,
  15. PlayCircleOutlined,
  16. PlusOutlined,
  17. StopOutlined,
  18. SyncOutlined,
  19. } from '@ant-design/icons';
  20. import { model } from '@formily/reactive';
  21. import Service from '@/pages/device/Instance/service';
  22. import type { MetadataItem } from '@/pages/device/Product/typings';
  23. import Save from './Save';
  24. import Export from './Export';
  25. import Import from './Import';
  26. import Process from './Process';
  27. import SearchComponent from '@/components/SearchComponent';
  28. import { PermissionButton, ProTableCard } from '@/components';
  29. import SystemConst from '@/utils/const';
  30. import Token from '@/utils/token';
  31. import DeviceCard from '@/components/ProTableCard/CardItems/device';
  32. import { getMenuPathByParams, MENUS_CODE } from '@/utils/menu';
  33. import { useLocation } from '@/hooks';
  34. import { service as deptService } from '@/pages/system/Department';
  35. import { service as categoryService } from '@/pages/device/Category';
  36. export const statusMap = new Map();
  37. statusMap.set('在线', 'success');
  38. statusMap.set('离线', 'error');
  39. statusMap.set('未激活', 'processing');
  40. statusMap.set('online', 'success');
  41. statusMap.set('offline', 'error');
  42. statusMap.set('notActive', 'processing');
  43. export const InstanceModel = model<{
  44. current: Partial<DeviceInstance>;
  45. detail: Partial<DeviceInstance>;
  46. config: any;
  47. metadataItem: MetadataItem;
  48. params: Set<string>; // 处理无限循环Card
  49. active?: string; // 当前编辑的Card
  50. }>({
  51. current: {},
  52. detail: {},
  53. config: {},
  54. metadataItem: {},
  55. params: new Set<string>(['test']),
  56. });
  57. export const service = new Service('device-instance');
  58. const Instance = () => {
  59. const actionRef = useRef<ActionType>();
  60. const [visible, setVisible] = useState<boolean>(false);
  61. const [exportVisible, setExportVisible] = useState<boolean>(false);
  62. const [importVisible, setImportVisible] = useState<boolean>(false);
  63. const [operationVisible, setOperationVisible] = useState<boolean>(false);
  64. const [type, setType] = useState<'active' | 'sync'>('active');
  65. const [api, setApi] = useState<string>('');
  66. const [current, setCurrent] = useState<Partial<DeviceInstance>>({});
  67. const [searchParams, setSearchParams] = useState<any>({});
  68. const [bindKeys, setBindKeys] = useState<any[]>([]);
  69. const history = useHistory<Record<string, string>>();
  70. const { permission } = PermissionButton.usePermission('device/Instance');
  71. const [jumpParams, setJumpParams] = useState<SearchTermsServer | undefined>(undefined);
  72. const intl = useIntl();
  73. const location = useLocation();
  74. useEffect(() => {
  75. if (location.state) {
  76. const _terms: any[] = [];
  77. Object.keys(location.state).forEach((key) => {
  78. _terms.push({
  79. column: key,
  80. value: location.state[key],
  81. });
  82. });
  83. setJumpParams([
  84. {
  85. terms: _terms,
  86. type: 'or',
  87. },
  88. ]);
  89. if (location.state && location.state?.save) {
  90. setVisible(true);
  91. setCurrent({});
  92. } else if (location.state && location.state?.import) {
  93. setImportVisible(true);
  94. }
  95. }
  96. }, [location]);
  97. const tools = (record: DeviceInstance) => [
  98. <Button
  99. type={'link'}
  100. style={{ padding: 0 }}
  101. key={'detail'}
  102. onClick={() => {
  103. InstanceModel.current = record;
  104. const url = getMenuPathByParams(MENUS_CODE['device/Instance/Detail'], record.id);
  105. history.push(url);
  106. }}
  107. >
  108. <Tooltip
  109. title={intl.formatMessage({
  110. id: 'pages.data.option.detail',
  111. defaultMessage: '查看',
  112. })}
  113. >
  114. <EyeOutlined />
  115. </Tooltip>
  116. </Button>,
  117. <PermissionButton
  118. type={'link'}
  119. key={'state'}
  120. style={{ padding: 0 }}
  121. popConfirm={{
  122. title: intl.formatMessage({
  123. id: `pages.data.option.${
  124. record.state.value !== 'notActive' ? 'disabled' : 'enabled'
  125. }.tips`,
  126. defaultMessage: '确认禁用?',
  127. }),
  128. onConfirm: () => {
  129. if (record.state.value !== 'notActive') {
  130. service.undeployDevice(record.id).then((resp: any) => {
  131. if (resp.status === 200) {
  132. message.success(
  133. intl.formatMessage({
  134. id: 'pages.data.option.success',
  135. defaultMessage: '操作成功!',
  136. }),
  137. );
  138. actionRef.current?.reload();
  139. }
  140. });
  141. } else {
  142. service.deployDevice(record.id).then((resp: any) => {
  143. if (resp.status === 200) {
  144. message.success(
  145. intl.formatMessage({
  146. id: 'pages.data.option.success',
  147. defaultMessage: '操作成功!',
  148. }),
  149. );
  150. actionRef.current?.reload();
  151. }
  152. });
  153. }
  154. },
  155. }}
  156. isPermission={permission.action}
  157. tooltip={{
  158. title: intl.formatMessage({
  159. id: `pages.data.option.${record.state.value !== 'notActive' ? 'disabled' : 'enabled'}`,
  160. defaultMessage: record.state.value !== 'notActive' ? '禁用' : '启用',
  161. }),
  162. }}
  163. >
  164. {record.state.value !== 'notActive' ? <StopOutlined /> : <PlayCircleOutlined />}
  165. </PermissionButton>,
  166. <PermissionButton
  167. type={'link'}
  168. key={'delete'}
  169. style={{ padding: 0 }}
  170. isPermission={permission.delete}
  171. tooltip={
  172. record.state.value !== 'notActive'
  173. ? { title: intl.formatMessage({ id: 'pages.device.instance.deleteTip' }) }
  174. : undefined
  175. }
  176. disabled={record.state.value !== 'notActive'}
  177. popConfirm={{
  178. title: intl.formatMessage({
  179. id: 'pages.data.option.remove.tips',
  180. }),
  181. disabled: record.state.value !== 'notActive',
  182. onConfirm: async () => {
  183. if (record.state.value === 'notActive') {
  184. await service.remove(record.id);
  185. message.success(
  186. intl.formatMessage({
  187. id: 'pages.data.option.success',
  188. defaultMessage: '操作成功!',
  189. }),
  190. );
  191. actionRef.current?.reload();
  192. } else {
  193. message.error(intl.formatMessage({ id: 'pages.device.instance.deleteTip' }));
  194. }
  195. },
  196. }}
  197. >
  198. <DeleteOutlined />
  199. </PermissionButton>,
  200. ];
  201. const columns: ProColumns<DeviceInstance>[] = [
  202. {
  203. title: 'ID',
  204. dataIndex: 'id',
  205. width: 300,
  206. ellipsis: true,
  207. fixed: 'left',
  208. },
  209. {
  210. title: intl.formatMessage({
  211. id: 'pages.table.deviceName',
  212. defaultMessage: '设备名称',
  213. }),
  214. dataIndex: 'name',
  215. ellipsis: true,
  216. width: 200,
  217. },
  218. {
  219. title: intl.formatMessage({
  220. id: 'pages.table.productName',
  221. defaultMessage: '产品名称',
  222. }),
  223. dataIndex: 'productId',
  224. width: 200,
  225. ellipsis: true,
  226. valueType: 'select',
  227. request: async () => {
  228. const res = await service.getProductList();
  229. if (res.status === 200) {
  230. return res.result.map((pItem: any) => ({ label: pItem.name, value: pItem.id }));
  231. }
  232. return [];
  233. },
  234. render: (_, row) => row.productName,
  235. filterMultiple: true,
  236. },
  237. {
  238. title: intl.formatMessage({
  239. id: 'pages.device.instance.registrationTime',
  240. defaultMessage: '注册时间',
  241. }),
  242. dataIndex: 'registryTime',
  243. width: '200px',
  244. valueType: 'dateTime',
  245. render: (text: any) => (text ? moment(text).format('YYYY-MM-DD HH:mm:ss') : '/'),
  246. sorter: true,
  247. },
  248. {
  249. title: intl.formatMessage({
  250. id: 'pages.searchTable.titleStatus',
  251. defaultMessage: '状态',
  252. }),
  253. dataIndex: 'state',
  254. width: '90px',
  255. valueType: 'select',
  256. renderText: (record) =>
  257. record ? <Badge status={statusMap.get(record.value)} text={record.text} /> : '',
  258. valueEnum: {
  259. notActive: {
  260. text: intl.formatMessage({
  261. id: 'pages.device.instance.status.notActive',
  262. defaultMessage: '未启用',
  263. }),
  264. status: 'notActive',
  265. },
  266. offline: {
  267. text: intl.formatMessage({
  268. id: 'pages.device.instance.status.offLine',
  269. defaultMessage: '离线',
  270. }),
  271. status: 'offline',
  272. },
  273. online: {
  274. text: intl.formatMessage({
  275. id: 'pages.device.instance.status.onLine',
  276. defaultMessage: '在线',
  277. }),
  278. status: 'online',
  279. },
  280. },
  281. filterMultiple: false,
  282. },
  283. {
  284. dataIndex: 'categoryId',
  285. title: '产品分类',
  286. valueType: 'treeSelect',
  287. hideInTable: true,
  288. fieldProps: {
  289. fieldNames: {
  290. label: 'name',
  291. value: 'id',
  292. },
  293. },
  294. request: () =>
  295. categoryService
  296. .queryTree({
  297. paging: false,
  298. })
  299. .then((resp: any) => resp.result),
  300. },
  301. {
  302. dataIndex: 'productId$product-info',
  303. title: '接入方式',
  304. valueType: 'select',
  305. hideInTable: true,
  306. request: () =>
  307. service.queryGatewayList().then((resp: any) =>
  308. resp.result.map((item: any) => ({
  309. label: item.name,
  310. value: `accessId is ${item.id}`,
  311. })),
  312. ),
  313. },
  314. {
  315. dataIndex: 'deviceType',
  316. title: '设备类型',
  317. valueType: 'select',
  318. hideInTable: true,
  319. valueEnum: {
  320. device: {
  321. text: '直连设备',
  322. status: 'device',
  323. },
  324. childrenDevice: {
  325. text: '网关子设备',
  326. status: 'childrenDevice',
  327. },
  328. gateway: {
  329. text: '网关设备',
  330. status: 'gateway',
  331. },
  332. },
  333. },
  334. {
  335. dataIndex: 'id$dim-assets',
  336. title: '所属部门',
  337. valueType: 'treeSelect',
  338. hideInTable: true,
  339. fieldProps: {
  340. fieldNames: {
  341. label: 'name',
  342. value: 'value',
  343. },
  344. },
  345. request: () =>
  346. deptService
  347. .queryOrgThree({
  348. paging: false,
  349. })
  350. .then((resp) => {
  351. const formatValue = (list: any[]) => {
  352. const _list: any[] = [];
  353. list.forEach((item) => {
  354. if (item.children) {
  355. item.children = formatValue(item.children);
  356. }
  357. _list.push({
  358. ...item,
  359. value: JSON.stringify({
  360. assetType: 'device',
  361. targets: [
  362. {
  363. type: 'org',
  364. id: item.id,
  365. },
  366. ],
  367. }),
  368. });
  369. });
  370. return _list;
  371. };
  372. return formatValue(resp.result);
  373. }),
  374. },
  375. {
  376. title: intl.formatMessage({
  377. id: 'pages.table.description',
  378. defaultMessage: '说明',
  379. }),
  380. dataIndex: 'describe',
  381. width: '15%',
  382. ellipsis: true,
  383. hideInSearch: true,
  384. },
  385. {
  386. title: intl.formatMessage({
  387. id: 'pages.data.option',
  388. defaultMessage: '操作',
  389. }),
  390. valueType: 'option',
  391. width: 120,
  392. fixed: 'right',
  393. render: (text, record) => tools(record),
  394. },
  395. ];
  396. const menu = (
  397. <Menu>
  398. <Menu.Item key="1">
  399. <PermissionButton
  400. isPermission={permission.export}
  401. icon={<ExportOutlined />}
  402. type="default"
  403. onClick={() => {
  404. setExportVisible(true);
  405. }}
  406. >
  407. 批量导出设备
  408. </PermissionButton>
  409. </Menu.Item>
  410. <Menu.Item key="2">
  411. <PermissionButton
  412. isPermission={permission.import}
  413. icon={<ImportOutlined />}
  414. onClick={() => {
  415. setImportVisible(true);
  416. }}
  417. >
  418. 批量导入设备
  419. </PermissionButton>
  420. </Menu.Item>
  421. <Menu.Item key="4">
  422. <PermissionButton
  423. isPermission={permission.action}
  424. icon={<CheckCircleOutlined />}
  425. type="primary"
  426. ghost
  427. popConfirm={{
  428. title: '确认激活全部设备?',
  429. onConfirm: async () => {
  430. setType('active');
  431. const activeAPI = `/${
  432. SystemConst.API_BASE
  433. }/device-instance/deploy?:X_Access_Token=${Token.get()}`;
  434. setApi(activeAPI);
  435. setOperationVisible(true);
  436. },
  437. }}
  438. >
  439. 激活全部设备
  440. </PermissionButton>
  441. </Menu.Item>
  442. <Menu.Item key="5">
  443. <PermissionButton
  444. isPermission={true}
  445. icon={<SyncOutlined />}
  446. type="primary"
  447. onClick={() => {
  448. setType('sync');
  449. const syncAPI = `/${
  450. SystemConst.API_BASE
  451. }/device-instance/state/_sync?:X_Access_Token=${Token.get()}`;
  452. setApi(syncAPI);
  453. setOperationVisible(true);
  454. }}
  455. >
  456. 同步设备状态
  457. </PermissionButton>
  458. </Menu.Item>
  459. {bindKeys.length > 0 && (
  460. <Menu.Item key="3">
  461. <PermissionButton
  462. isPermission={permission.delete}
  463. icon={<DeleteOutlined />}
  464. type="primary"
  465. danger
  466. popConfirm={{
  467. title: '确认删除选中设备?',
  468. onConfirm: () => {
  469. service.batchDeleteDevice(bindKeys).then((resp) => {
  470. if (resp.status === 200) {
  471. message.success('操作成功');
  472. actionRef.current?.reset?.();
  473. }
  474. });
  475. },
  476. }}
  477. >
  478. 删除选中设备
  479. </PermissionButton>
  480. </Menu.Item>
  481. )}
  482. {bindKeys.length > 0 && (
  483. <Menu.Item key="6">
  484. <PermissionButton
  485. isPermission={permission.action}
  486. icon={<StopOutlined />}
  487. type="primary"
  488. danger
  489. popConfirm={{
  490. title: '确认禁用选中设备?',
  491. onConfirm() {
  492. service.batchUndeployDevice(bindKeys).then((resp) => {
  493. if (resp.status === 200) {
  494. message.success('操作成功');
  495. actionRef.current?.reset?.();
  496. }
  497. });
  498. },
  499. }}
  500. >
  501. 禁用选中设备
  502. </PermissionButton>
  503. </Menu.Item>
  504. )}
  505. </Menu>
  506. );
  507. return (
  508. <PageContainer>
  509. <SearchComponent<DeviceInstance>
  510. field={columns}
  511. target="device-instance"
  512. initParam={jumpParams}
  513. onSearch={(data) => {
  514. actionRef.current?.reset?.();
  515. setSearchParams(data);
  516. }}
  517. // onReset={() => {
  518. // // 重置分页及搜索参数
  519. // actionRef.current?.reset?.();
  520. // setSearchParams({});
  521. // }}
  522. />
  523. <ProTableCard<DeviceInstance>
  524. columns={columns}
  525. scroll={{ x: 1366 }}
  526. actionRef={actionRef}
  527. params={searchParams}
  528. options={{ fullScreen: true }}
  529. request={(params) =>
  530. service.query({
  531. ...params,
  532. sorts: [
  533. {
  534. name: 'createTime',
  535. order: 'desc',
  536. },
  537. ],
  538. })
  539. }
  540. rowKey="id"
  541. search={false}
  542. pagination={{ pageSize: 10 }}
  543. rowSelection={{
  544. selectedRowKeys: bindKeys,
  545. onChange: (selectedRowKeys, selectedRows) => {
  546. setBindKeys(selectedRows.map((item) => item.id));
  547. },
  548. }}
  549. headerTitle={[
  550. <PermissionButton
  551. onClick={() => {
  552. setVisible(true);
  553. setCurrent({});
  554. }}
  555. style={{ marginRight: 12 }}
  556. isPermission={permission.add}
  557. key="button"
  558. icon={<PlusOutlined />}
  559. type="primary"
  560. >
  561. {intl.formatMessage({
  562. id: 'pages.data.option.add',
  563. defaultMessage: '新增',
  564. })}
  565. </PermissionButton>,
  566. <Dropdown key={'more'} overlay={menu} placement="bottom">
  567. <Button>批量操作</Button>
  568. </Dropdown>,
  569. ]}
  570. cardRender={(record) => (
  571. <DeviceCard
  572. {...record}
  573. detail={
  574. <div
  575. style={{ padding: 8, fontSize: 24 }}
  576. onClick={() => {
  577. InstanceModel.current = record;
  578. const url = getMenuPathByParams(MENUS_CODE['device/Instance/Detail'], record.id);
  579. history.push(url);
  580. }}
  581. >
  582. <EyeOutlined />
  583. </div>
  584. }
  585. actions={[
  586. <PermissionButton
  587. type={'link'}
  588. onClick={() => {
  589. setCurrent(record);
  590. setVisible(true);
  591. }}
  592. key={'edit'}
  593. isPermission={permission.update}
  594. >
  595. <EditOutlined />
  596. {intl.formatMessage({
  597. id: 'pages.data.option.edit',
  598. defaultMessage: '编辑',
  599. })}
  600. </PermissionButton>,
  601. <PermissionButton
  602. isPermission={permission.action}
  603. type={'link'}
  604. key={'state'}
  605. style={{ padding: 0 }}
  606. popConfirm={{
  607. title: intl.formatMessage({
  608. id: `pages.data.option.${
  609. record.state.value !== 'notActive' ? 'disabled' : 'enabled'
  610. }.tips`,
  611. defaultMessage: '确认禁用?',
  612. }),
  613. onConfirm: async () => {
  614. if (record.state.value !== 'notActive') {
  615. await service.undeployDevice(record.id);
  616. } else {
  617. await service.deployDevice(record.id);
  618. }
  619. message.success(
  620. intl.formatMessage({
  621. id: 'pages.data.option.success',
  622. defaultMessage: '操作成功!',
  623. }),
  624. );
  625. actionRef.current?.reload();
  626. },
  627. }}
  628. >
  629. {record.state.value !== 'notActive' ? <StopOutlined /> : <PlayCircleOutlined />}
  630. {intl.formatMessage({
  631. id: `pages.data.option.${
  632. record.state.value !== 'notActive' ? 'disabled' : 'enabled'
  633. }`,
  634. defaultMessage: record.state.value !== 'notActive' ? '禁用' : '启用',
  635. })}
  636. </PermissionButton>,
  637. <PermissionButton
  638. key="delete"
  639. isPermission={permission.delete}
  640. type={'link'}
  641. style={{ padding: 0 }}
  642. tooltip={
  643. record.state.value !== 'notActive'
  644. ? { title: intl.formatMessage({ id: 'pages.device.instance.deleteTip' }) }
  645. : undefined
  646. }
  647. disabled={record.state.value !== 'notActive'}
  648. popConfirm={{
  649. title: intl.formatMessage({
  650. id: 'pages.data.option.remove.tips',
  651. }),
  652. disabled: record.state.value !== 'notActive',
  653. onConfirm: async () => {
  654. if (record.state.value === 'notActive') {
  655. await service.remove(record.id);
  656. message.success(
  657. intl.formatMessage({
  658. id: 'pages.data.option.success',
  659. defaultMessage: '操作成功!',
  660. }),
  661. );
  662. actionRef.current?.reload();
  663. } else {
  664. message.error(intl.formatMessage({ id: 'pages.device.instance.deleteTip' }));
  665. }
  666. },
  667. }}
  668. >
  669. <DeleteOutlined />
  670. </PermissionButton>,
  671. ]}
  672. />
  673. )}
  674. />
  675. <Save
  676. data={current}
  677. model={!Object.keys(current).length ? 'add' : 'edit'}
  678. close={() => {
  679. setVisible(false);
  680. }}
  681. reload={() => {
  682. actionRef.current?.reload();
  683. }}
  684. visible={visible}
  685. />
  686. <Export
  687. data={searchParams}
  688. close={() => {
  689. setExportVisible(false);
  690. actionRef.current?.reload();
  691. }}
  692. visible={exportVisible}
  693. />
  694. <Import
  695. data={current}
  696. close={() => {
  697. setImportVisible(false);
  698. actionRef.current?.reload();
  699. }}
  700. visible={importVisible}
  701. />
  702. {operationVisible && (
  703. <Process
  704. api={api}
  705. action={type}
  706. closeVisible={() => {
  707. setOperationVisible(false);
  708. actionRef.current?.reload();
  709. }}
  710. />
  711. )}
  712. </PageContainer>
  713. );
  714. };
  715. export default Instance;