index.tsx 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596
  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, Popconfirm, Tooltip } from 'antd';
  6. import { useRef, useState } from 'react';
  7. import { useHistory } from 'umi';
  8. import {
  9. CheckCircleOutlined,
  10. DeleteOutlined,
  11. EditOutlined,
  12. ExportOutlined,
  13. EyeOutlined,
  14. ImportOutlined,
  15. PlusOutlined,
  16. StopOutlined,
  17. SyncOutlined,
  18. } from '@ant-design/icons';
  19. import { model } from '@formily/reactive';
  20. import Service from '@/pages/device/Instance/service';
  21. import type { MetadataItem } from '@/pages/device/Product/typings';
  22. import { useIntl } from '@@/plugin-locale/localeExports';
  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 { 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 { getButtonPermission, getMenuPathByParams, MENUS_CODE } from '@/utils/menu';
  33. export const statusMap = new Map();
  34. statusMap.set('在线', 'success');
  35. statusMap.set('离线', 'error');
  36. statusMap.set('未激活', 'processing');
  37. statusMap.set('online', 'success');
  38. statusMap.set('offline', 'error');
  39. statusMap.set('notActive', 'processing');
  40. export const InstanceModel = model<{
  41. current: Partial<DeviceInstance>;
  42. detail: Partial<DeviceInstance>;
  43. config: any;
  44. metadataItem: MetadataItem;
  45. params: Set<string>; // 处理无限循环Card
  46. active?: string; // 当前编辑的Card
  47. }>({
  48. current: {},
  49. detail: {},
  50. config: {},
  51. metadataItem: {},
  52. params: new Set<string>(['test']),
  53. });
  54. export const service = new Service('device-instance');
  55. const Instance = () => {
  56. const actionRef = useRef<ActionType>();
  57. const [visible, setVisible] = useState<boolean>(false);
  58. const [exportVisible, setExportVisible] = useState<boolean>(false);
  59. const [importVisible, setImportVisible] = useState<boolean>(false);
  60. const [operationVisible, setOperationVisible] = useState<boolean>(false);
  61. const [type, setType] = useState<'active' | 'sync'>('active');
  62. const [api, setApi] = useState<string>('');
  63. const [current, setCurrent] = useState<Partial<DeviceInstance>>({});
  64. const [searchParams, setSearchParams] = useState<any>({});
  65. const [bindKeys, setBindKeys] = useState<any[]>([]);
  66. const history = useHistory<Record<string, string>>();
  67. const intl = useIntl();
  68. const tools = (record: DeviceInstance) => [
  69. <Button
  70. type={'link'}
  71. style={{ padding: 0 }}
  72. key={'detail'}
  73. onClick={() => {
  74. InstanceModel.current = record;
  75. const url = getMenuPathByParams(MENUS_CODE['device/Instance/Detail'], record.id);
  76. history.push(url);
  77. }}
  78. >
  79. <Tooltip
  80. title={intl.formatMessage({
  81. id: 'pages.data.option.detail',
  82. defaultMessage: '查看',
  83. })}
  84. >
  85. <EyeOutlined />
  86. </Tooltip>
  87. </Button>,
  88. <Button
  89. type={'link'}
  90. key={'state'}
  91. style={{ padding: 0 }}
  92. disabled={getButtonPermission('device/Product', ['action'])}
  93. >
  94. <Popconfirm
  95. title={intl.formatMessage({
  96. id: `pages.data.option.${
  97. record.state.value !== 'notActive' ? 'disabled' : 'enabled'
  98. }.tips`,
  99. defaultMessage: '确认禁用?',
  100. })}
  101. onConfirm={async () => {
  102. if (record.state.value !== 'notActive') {
  103. await service.undeployDevice(record.id);
  104. } else {
  105. await service.deployDevice(record.id);
  106. }
  107. message.success(
  108. intl.formatMessage({
  109. id: 'pages.data.option.success',
  110. defaultMessage: '操作成功!',
  111. }),
  112. );
  113. actionRef.current?.reload();
  114. }}
  115. >
  116. <Tooltip
  117. title={intl.formatMessage({
  118. id: `pages.data.option.${record.state.value !== 'notActive' ? 'disabled' : 'enabled'}`,
  119. defaultMessage: record.state.value !== 'notActive' ? '禁用' : '启用',
  120. })}
  121. >
  122. {record.state.value !== 'notActive' ? <StopOutlined /> : <CheckCircleOutlined />}
  123. </Tooltip>
  124. </Popconfirm>{' '}
  125. </Button>,
  126. <Button
  127. type={'link'}
  128. key={'delete'}
  129. style={{ padding: 0 }}
  130. disabled={getButtonPermission('device/Instance', ['delete'])}
  131. >
  132. <Popconfirm
  133. title={intl.formatMessage({
  134. id:
  135. record.state.value === 'notActive'
  136. ? 'pages.data.option.remove.tips'
  137. : 'pages.device.instance.deleteTip',
  138. })}
  139. onConfirm={async () => {
  140. if (record.state.value === 'notActive') {
  141. await service.remove(record.id);
  142. message.success(
  143. intl.formatMessage({
  144. id: 'pages.data.option.success',
  145. defaultMessage: '操作成功!',
  146. }),
  147. );
  148. actionRef.current?.reload();
  149. } else {
  150. message.error(intl.formatMessage({ id: 'pages.device.instance.deleteTip' }));
  151. }
  152. }}
  153. >
  154. <Tooltip
  155. title={intl.formatMessage({
  156. id: 'pages.data.option.remove',
  157. defaultMessage: '删除',
  158. })}
  159. >
  160. <DeleteOutlined />
  161. </Tooltip>
  162. </Popconfirm>
  163. </Button>,
  164. ];
  165. const columns: ProColumns<DeviceInstance>[] = [
  166. {
  167. title: 'ID',
  168. dataIndex: 'id',
  169. width: 300,
  170. ellipsis: true,
  171. },
  172. {
  173. title: intl.formatMessage({
  174. id: 'pages.table.deviceName',
  175. defaultMessage: '设备名称',
  176. }),
  177. dataIndex: 'name',
  178. ellipsis: true,
  179. width: 200,
  180. },
  181. {
  182. title: intl.formatMessage({
  183. id: 'pages.table.productName',
  184. defaultMessage: '产品名称',
  185. }),
  186. dataIndex: 'productName',
  187. width: 200,
  188. ellipsis: true,
  189. valueType: 'select',
  190. request: async () => {
  191. const res = await service.getProductList();
  192. if (res.status === 200) {
  193. return res.result.map((pItem: any) => ({ label: pItem.name, value: pItem.id }));
  194. }
  195. return [];
  196. },
  197. filterMultiple: true,
  198. },
  199. {
  200. title: intl.formatMessage({
  201. id: 'pages.device.instance.registrationTime',
  202. defaultMessage: '注册时间',
  203. }),
  204. dataIndex: 'registryTime',
  205. width: '200px',
  206. valueType: 'dateTime',
  207. render: (text: any) => (text ? moment(text).format('YYYY-MM-DD HH:mm:ss') : '/'),
  208. sorter: true,
  209. },
  210. {
  211. title: intl.formatMessage({
  212. id: 'pages.searchTable.titleStatus',
  213. defaultMessage: '状态',
  214. }),
  215. dataIndex: 'state',
  216. width: '90px',
  217. valueType: 'select',
  218. renderText: (record) =>
  219. record ? <Badge status={statusMap.get(record.value)} text={record.text} /> : '',
  220. valueEnum: {
  221. notActive: {
  222. text: intl.formatMessage({
  223. id: 'pages.device.instance.status.notActive',
  224. defaultMessage: '未启用',
  225. }),
  226. status: 'notActive',
  227. },
  228. offline: {
  229. text: intl.formatMessage({
  230. id: 'pages.device.instance.status.offLine',
  231. defaultMessage: '离线',
  232. }),
  233. status: 'offline',
  234. },
  235. online: {
  236. text: intl.formatMessage({
  237. id: 'pages.device.instance.status.onLine',
  238. defaultMessage: '在线',
  239. }),
  240. status: 'online',
  241. },
  242. },
  243. filterMultiple: false,
  244. },
  245. {
  246. title: intl.formatMessage({
  247. id: 'pages.table.description',
  248. defaultMessage: '说明',
  249. }),
  250. dataIndex: 'describe',
  251. width: '15%',
  252. ellipsis: true,
  253. hideInSearch: true,
  254. },
  255. {
  256. title: intl.formatMessage({
  257. id: 'pages.data.option',
  258. defaultMessage: '操作',
  259. }),
  260. valueType: 'option',
  261. width: 200,
  262. render: (text, record) => tools(record),
  263. },
  264. ];
  265. const menu = (
  266. <Menu>
  267. <Menu.Item key="1">
  268. <Button
  269. disabled={getButtonPermission('device/Instance', ['export'])}
  270. icon={<ExportOutlined />}
  271. type="default"
  272. onClick={() => {
  273. setExportVisible(true);
  274. }}
  275. >
  276. 批量导出设备
  277. </Button>
  278. </Menu.Item>
  279. <Menu.Item key="2">
  280. <Button
  281. disabled={getButtonPermission('device/Instance', ['import'])}
  282. icon={<ImportOutlined />}
  283. onClick={() => {
  284. setImportVisible(true);
  285. }}
  286. >
  287. 批量导入设备
  288. </Button>
  289. </Menu.Item>
  290. <Menu.Item key="4">
  291. <Button
  292. disabled={getButtonPermission('device/Instance', ['active'])}
  293. icon={<CheckCircleOutlined />}
  294. type="primary"
  295. ghost
  296. >
  297. <Popconfirm
  298. title={'确认激活全部设备?'}
  299. onConfirm={() => {
  300. setType('active');
  301. const activeAPI = `/${
  302. SystemConst.API_BASE
  303. }/device-instance/deploy?:X_Access_Token=${Token.get()}`;
  304. setApi(activeAPI);
  305. setOperationVisible(true);
  306. }}
  307. >
  308. 激活全部设备
  309. </Popconfirm>
  310. </Button>
  311. </Menu.Item>
  312. <Menu.Item key="5">
  313. <Button
  314. disabled={getButtonPermission('device/Instance', ['sync'])}
  315. icon={<SyncOutlined />}
  316. type="primary"
  317. onClick={() => {
  318. setType('sync');
  319. const syncAPI = `/${
  320. SystemConst.API_BASE
  321. }/device-instance/state/_sync?:X_Access_Token=${Token.get()}`;
  322. setApi(syncAPI);
  323. setOperationVisible(true);
  324. }}
  325. >
  326. 同步设备状态
  327. </Button>
  328. </Menu.Item>
  329. {bindKeys.length > 0 && (
  330. <Menu.Item key="3">
  331. <Button
  332. disabled={getButtonPermission('device/Instance', ['delete'])}
  333. icon={<DeleteOutlined />}
  334. type="primary"
  335. danger
  336. >
  337. <Popconfirm
  338. title="确认删除选中设备?"
  339. onConfirm={() => {
  340. service.batchDeleteDevice(bindKeys).then((resp) => {
  341. if (resp.status === 200) {
  342. message.success('操作成功');
  343. actionRef.current?.reset?.();
  344. }
  345. });
  346. }}
  347. okText="确认"
  348. cancelText="取消"
  349. >
  350. 删除选中设备
  351. </Popconfirm>
  352. </Button>
  353. </Menu.Item>
  354. )}
  355. {bindKeys.length > 0 && (
  356. <Menu.Item key="6">
  357. <Button
  358. disabled={getButtonPermission('device/Instance', ['action'])}
  359. icon={<StopOutlined />}
  360. type="primary"
  361. danger
  362. >
  363. <Popconfirm
  364. title="确认禁用选中设备?"
  365. onConfirm={() => {
  366. service.batchUndeployDevice(bindKeys).then((resp) => {
  367. if (resp.status === 200) {
  368. message.success('操作成功');
  369. actionRef.current?.reset?.();
  370. }
  371. });
  372. }}
  373. okText="确认"
  374. cancelText="取消"
  375. >
  376. 禁用选中设备
  377. </Popconfirm>
  378. </Button>
  379. </Menu.Item>
  380. )}
  381. </Menu>
  382. );
  383. return (
  384. <PageContainer>
  385. <SearchComponent<DeviceInstance>
  386. field={columns}
  387. target="device-instance"
  388. onSearch={(data) => {
  389. actionRef.current?.reset?.();
  390. setSearchParams(data);
  391. }}
  392. // onReset={() => {
  393. // // 重置分页及搜索参数
  394. // actionRef.current?.reset?.();
  395. // setSearchParams({});
  396. // }}
  397. />
  398. <ProTableCard<DeviceInstance>
  399. columns={columns}
  400. actionRef={actionRef}
  401. params={searchParams}
  402. options={{ fullScreen: true }}
  403. request={(params) =>
  404. service.query({
  405. ...params,
  406. sorts: [
  407. {
  408. name: 'createTime',
  409. order: 'desc',
  410. },
  411. ],
  412. })
  413. }
  414. rowKey="id"
  415. search={false}
  416. pagination={{ pageSize: 10 }}
  417. rowSelection={{
  418. selectedRowKeys: bindKeys,
  419. onChange: (selectedRowKeys, selectedRows) => {
  420. setBindKeys(selectedRows.map((item) => item.id));
  421. },
  422. }}
  423. headerTitle={[
  424. <Button
  425. onClick={() => {
  426. setVisible(true);
  427. setCurrent({});
  428. }}
  429. style={{ marginRight: 12 }}
  430. disabled={getButtonPermission('device/Instance', 'add')}
  431. key="button"
  432. icon={<PlusOutlined />}
  433. type="primary"
  434. >
  435. {intl.formatMessage({
  436. id: 'pages.data.option.add',
  437. defaultMessage: '新增',
  438. })}
  439. </Button>,
  440. <Dropdown key={'more'} overlay={menu} placement="bottom">
  441. <Button>批量操作</Button>
  442. </Dropdown>,
  443. ]}
  444. cardRender={(record) => (
  445. <DeviceCard
  446. {...record}
  447. detail={
  448. <div
  449. style={{ padding: 8, fontSize: 24 }}
  450. onClick={() => {
  451. InstanceModel.current = record;
  452. const url = getMenuPathByParams(MENUS_CODE['device/Instance/Detail'], record.id);
  453. history.push(url);
  454. }}
  455. >
  456. <EyeOutlined />
  457. </div>
  458. }
  459. actions={[
  460. <Button
  461. type={'link'}
  462. onClick={() => {
  463. setCurrent(record);
  464. setVisible(true);
  465. }}
  466. key={'edit'}
  467. disabled={getButtonPermission('device/Instance', ['update', 'add'])}
  468. >
  469. <EditOutlined />
  470. {intl.formatMessage({
  471. id: 'pages.data.option.edit',
  472. defaultMessage: '编辑',
  473. })}
  474. </Button>,
  475. <Button
  476. disabled={getButtonPermission('device/Instance', ['action'])}
  477. type={'link'}
  478. key={'state'}
  479. style={{ padding: 0 }}
  480. >
  481. <Popconfirm
  482. title={intl.formatMessage({
  483. id: `pages.data.option.${
  484. record.state.value !== 'notActive' ? 'disabled' : 'enabled'
  485. }.tips`,
  486. defaultMessage: '确认禁用?',
  487. })}
  488. onConfirm={async () => {
  489. if (record.state.value !== 'notActive') {
  490. await service.undeployDevice(record.id);
  491. } else {
  492. await service.deployDevice(record.id);
  493. }
  494. message.success(
  495. intl.formatMessage({
  496. id: 'pages.data.option.success',
  497. defaultMessage: '操作成功!',
  498. }),
  499. );
  500. actionRef.current?.reload();
  501. }}
  502. >
  503. {record.state.value !== 'notActive' ? <StopOutlined /> : <CheckCircleOutlined />}
  504. {intl.formatMessage({
  505. id: `pages.data.option.${
  506. record.state.value !== 'notActive' ? 'disabled' : 'enabled'
  507. }`,
  508. defaultMessage: record.state.value !== 'notActive' ? '禁用' : '启用',
  509. })}
  510. </Popconfirm>
  511. </Button>,
  512. <Button
  513. key="delete"
  514. disabled={getButtonPermission('device/Instance', ['delete'])}
  515. type={'link'}
  516. style={{ padding: 0 }}
  517. >
  518. <Popconfirm
  519. title={intl.formatMessage({
  520. id:
  521. record.state.value === 'notActive'
  522. ? 'pages.data.option.remove.tips'
  523. : 'pages.device.instance.deleteTip',
  524. })}
  525. key={'delete'}
  526. onConfirm={async () => {
  527. if (record.state.value === 'notActive') {
  528. await service.remove(record.id);
  529. message.success(
  530. intl.formatMessage({
  531. id: 'pages.data.option.success',
  532. defaultMessage: '操作成功!',
  533. }),
  534. );
  535. actionRef.current?.reload();
  536. } else {
  537. message.error(intl.formatMessage({ id: 'pages.device.instance.deleteTip' }));
  538. }
  539. }}
  540. >
  541. <DeleteOutlined />
  542. </Popconfirm>
  543. </Button>,
  544. ]}
  545. />
  546. )}
  547. />
  548. <Save
  549. data={current}
  550. model={!Object.keys(current).length ? 'add' : 'edit'}
  551. close={() => {
  552. setVisible(false);
  553. }}
  554. reload={() => {
  555. actionRef.current?.reload();
  556. }}
  557. visible={visible}
  558. />
  559. <Export
  560. data={searchParams}
  561. close={() => {
  562. setExportVisible(false);
  563. actionRef.current?.reload();
  564. }}
  565. visible={exportVisible}
  566. />
  567. <Import
  568. data={current}
  569. close={() => {
  570. setImportVisible(false);
  571. actionRef.current?.reload();
  572. }}
  573. visible={importVisible}
  574. />
  575. {operationVisible && (
  576. <Process
  577. api={api}
  578. action={type}
  579. closeVisible={() => {
  580. setOperationVisible(false);
  581. actionRef.current?.reload();
  582. }}
  583. />
  584. )}
  585. </PageContainer>
  586. );
  587. };
  588. export default Instance;