index.tsx 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481
  1. import { PageContainer } from '@ant-design/pro-layout';
  2. import { InstanceModel } from '@/pages/device/Instance';
  3. import { history, useParams } from 'umi';
  4. import { Badge, Card, Descriptions, Divider, message, Space, Tooltip } from 'antd';
  5. import type { ReactNode } from 'react';
  6. import { useEffect, useState } from 'react';
  7. import { observer } from '@formily/react';
  8. import Log from '@/pages/device/Instance/Detail/Log';
  9. // import Alarm from '@/pages/device/components/Alarm';
  10. import Info from '@/pages/device/Instance/Detail/Info';
  11. import Functions from '@/pages/device/Instance/Detail/Functions';
  12. import Running from '@/pages/device/Instance/Detail/Running';
  13. import ChildDevice from '@/pages/device/Instance/Detail/ChildDevice';
  14. import Diagnose from '@/pages/device/Instance/Detail/Diagnose';
  15. import MetadataMap from '@/pages/device/Instance/Detail/MetadataMap';
  16. import Opcua from '@/pages/device/Instance/Detail/Opcua';
  17. import Modbus from '@/pages/device/Instance/Detail/Modbus';
  18. import { useIntl } from '@@/plugin-locale/localeExports';
  19. import Metadata from '../../components/Metadata';
  20. import type { DeviceMetadata } from '@/pages/device/Product/typings';
  21. import MetadataAction from '@/pages/device/components/Metadata/DataBaseAction';
  22. import { Store } from 'jetlinks-store';
  23. import SystemConst from '@/utils/const';
  24. import { getMenuPathByCode, getMenuPathByParams, MENUS_CODE } from '@/utils/menu';
  25. import useSendWebsocketMessage from '@/hooks/websocket/useSendWebsocketMessage';
  26. import { Ellipsis, PermissionButton } from '@/components';
  27. import { QuestionCircleOutlined } from '@ant-design/icons';
  28. import Service from '@/pages/device/Instance/service';
  29. import useLocation from '@/hooks/route/useLocation';
  30. import { onlyMessage } from '@/utils/util';
  31. import Parsing from './Parsing';
  32. // import Editable from './Modbus/editTable';
  33. export const deviceStatus = new Map();
  34. deviceStatus.set('online', <Badge status="success" text={'在线'} />);
  35. deviceStatus.set('offline', <Badge status="error" text={'离线'} />);
  36. deviceStatus.set('notActive', <Badge status="processing" text={'禁用'} />);
  37. const InstanceDetail = observer(() => {
  38. const intl = useIntl();
  39. // const [tab, setTab] = useState<string>('detail');
  40. const params = useParams<{ id: string }>();
  41. const service = new Service('device-instance');
  42. const { permission } = PermissionButton.usePermission('device/Instance');
  43. const location = useLocation();
  44. // const resetMetadata = async () => {
  45. // const resp = await service.deleteMetadata(params.id);
  46. // if (resp.status === 200) {
  47. // message.success('操作成功');
  48. // Store.set(SystemConst.REFRESH_DEVICE, true);
  49. // setTimeout(() => {
  50. // Store.set(SystemConst.REFRESH_METADATA_TABLE, true);
  51. // }, 400);
  52. // }
  53. // };
  54. const baseList = [
  55. {
  56. key: 'detail',
  57. tab: intl.formatMessage({
  58. id: 'pages.device.instanceDetail.detail',
  59. defaultMessage: '实例信息',
  60. }),
  61. component: <Info />,
  62. },
  63. {
  64. key: 'running',
  65. tab: intl.formatMessage({
  66. id: 'pages.device.instanceDetail.running',
  67. defaultMessage: '运行状态',
  68. }),
  69. component: <Running />,
  70. },
  71. {
  72. key: 'metadata',
  73. tab: (
  74. <>
  75. {intl.formatMessage({
  76. id: 'pages.device.instanceDetail.metadata',
  77. defaultMessage: '物模型',
  78. })}
  79. <Tooltip
  80. title={
  81. <>
  82. 属性:
  83. <br />
  84. 用于描述设备运行时具体信息和状态。
  85. <br />
  86. 功能:
  87. <br />
  88. 指设备可供外部调用的指令或方法。
  89. <br />
  90. 事件:
  91. <br />
  92. 设备运行时,主动上报给云端的信息。
  93. <br />
  94. 标签:
  95. <br />
  96. 统一为设备添加拓展字段,添加后将在设备信息页显示。
  97. </>
  98. }
  99. >
  100. <QuestionCircleOutlined style={{ marginLeft: 5 }} />
  101. </Tooltip>
  102. </>
  103. ),
  104. component: (
  105. <Card>
  106. <Metadata
  107. type="device"
  108. // tabAction={
  109. // <PermissionButton
  110. // isPermission={permission.update}
  111. // popConfirm={{
  112. // title: '确认重置?',
  113. // onConfirm: resetMetadata,
  114. // }}
  115. // tooltip={{
  116. // title: '重置后将使用产品的物模型配置',
  117. // }}
  118. // key={'reload'}
  119. // >
  120. // 重置操作1
  121. // </PermissionButton>
  122. // }
  123. />
  124. </Card>
  125. ),
  126. },
  127. {
  128. // 物模型中有事件信息,且设备状态是在线的情况下才显示此模块
  129. key: 'functions',
  130. tab: intl.formatMessage({
  131. id: 'pages.device.instanceDetail.functions',
  132. defaultMessage: '设备功能',
  133. }),
  134. component: <Functions />,
  135. },
  136. {
  137. key: 'log',
  138. tab: intl.formatMessage({
  139. id: 'pages.device.instanceDetail.log',
  140. defaultMessage: '日志管理',
  141. }),
  142. component: <Log />,
  143. },
  144. {
  145. key: 'diagnose',
  146. tab: '设备诊断',
  147. component: <Diagnose />,
  148. },
  149. ];
  150. const pList = [
  151. 'websocket-server',
  152. 'http-server-gateway',
  153. 'udp-device-gateway',
  154. 'coap-server-gateway',
  155. 'mqtt-client-gateway',
  156. 'mqtt-server-gateway',
  157. 'tcp-server-gateway',
  158. ];
  159. const [list, setList] =
  160. useState<{ key: string; tab: string | ReactNode; component: ReactNode }[]>(baseList);
  161. const getDetail = async (id: string) => {
  162. const response = await service.detail(id);
  163. if (response.status === 200) {
  164. InstanceModel.detail = response?.result;
  165. const datalist = [...baseList];
  166. if (
  167. InstanceModel.detail?.accessProvider &&
  168. pList.includes(InstanceModel.detail?.accessProvider)
  169. ) {
  170. datalist.push({
  171. key: 'metadata-map',
  172. tab: '物模型映射',
  173. component: <MetadataMap type="device" />,
  174. });
  175. }
  176. const paring = response.result?.features?.find((item: any) => item.id === 'transparentCodec');
  177. if (paring) {
  178. datalist.push({
  179. key: 'parsing',
  180. tab: intl.formatMessage({
  181. id: 'pages.device.instanceDetail.parsing',
  182. defaultMessage: '数据解析',
  183. }),
  184. component: <Parsing tag="device" data={InstanceModel.detail} />,
  185. });
  186. }
  187. if (response.result.protocol === 'modbus-tcp') {
  188. datalist.push({
  189. key: 'modbus',
  190. tab: 'Modbus',
  191. component: <Modbus />,
  192. // component: <Editable />,
  193. });
  194. }
  195. if (response.result.protocol === 'opc-ua') {
  196. datalist.push({
  197. key: 'opcua',
  198. tab: 'OPC UA',
  199. component: <Opcua />,
  200. });
  201. }
  202. if (response.result.deviceType?.value === 'gateway') {
  203. // 产品类型为网关的情况下才显示此模块
  204. datalist.push({
  205. key: 'child-device',
  206. tab: '子设备',
  207. component: <ChildDevice />,
  208. });
  209. }
  210. setList(datalist);
  211. // 写入物模型数据
  212. const metadata: DeviceMetadata = JSON.parse(response.result?.metadata || '{}');
  213. MetadataAction.insert(metadata);
  214. }
  215. };
  216. const [subscribeTopic] = useSendWebsocketMessage();
  217. useEffect(() => {
  218. if (subscribeTopic) {
  219. subscribeTopic(
  220. `instance-editor-info-status-${params.id}`,
  221. `/dashboard/device/status/change/realTime`,
  222. {
  223. deviceId: params.id,
  224. },
  225. // @ts-ignore
  226. ).subscribe((data: any) => {
  227. const payload = data.payload;
  228. const state = payload.value.type;
  229. InstanceModel.detail.state = {
  230. value: state,
  231. text: '',
  232. };
  233. });
  234. }
  235. }, []);
  236. useEffect(() => {
  237. Store.subscribe(SystemConst.REFRESH_DEVICE, () => {
  238. MetadataAction.clean();
  239. setTimeout(() => {
  240. getDetail(params.id);
  241. }, 200);
  242. });
  243. // return subscription.unsubscribe();
  244. }, []);
  245. useEffect(() => {
  246. // console.log(InstanceModel.current)
  247. if (!InstanceModel.current && !params.id) {
  248. history.goBack();
  249. } else {
  250. // setTab('detail');
  251. InstanceModel.active = 'detail';
  252. getDetail(params?.id || InstanceModel.current?.id || '');
  253. }
  254. return () => {
  255. MetadataAction.clean();
  256. };
  257. }, [params.id]);
  258. useEffect(() => {
  259. if ((location as any).query?.key) {
  260. // setTab((location as any).query?.key || 'detail');
  261. InstanceModel.active = (location as any).query?.key || 'detail';
  262. }
  263. const subscription = Store.subscribe(SystemConst.BASE_UPDATE_DATA, (data) => {
  264. if ((window as any).onTabSaveSuccess) {
  265. (window as any).onTabSaveSuccess(data);
  266. setTimeout(() => window.close(), 300);
  267. }
  268. });
  269. return () => subscription.unsubscribe();
  270. }, []);
  271. useEffect(() => {
  272. const { state } = location;
  273. if (state && state?.tab) {
  274. // setTab(state?.tab);
  275. InstanceModel.active = state?.tab;
  276. }
  277. }, [location]);
  278. return (
  279. <PageContainer
  280. className={'page-title-show'}
  281. onBack={history.goBack}
  282. onTabChange={(e) => {
  283. InstanceModel.active = e;
  284. }}
  285. tabList={list}
  286. tabActiveKey={InstanceModel.active}
  287. content={
  288. <Descriptions size="small" column={4}>
  289. <Descriptions.Item label={'ID'}>
  290. <Ellipsis
  291. title={InstanceModel.detail?.id}
  292. tooltip={{ placement: 'topLeft' }}
  293. style={{ maxWidth: 250 }}
  294. limitWidth={250}
  295. />
  296. {/*<Tooltip placement="topLeft" title={InstanceModel.detail?.id}>*/}
  297. {/* <div className="ellipsis" style={{ maxWidth: 250 }}>*/}
  298. {/* {InstanceModel.detail?.id}*/}
  299. {/* </div>*/}
  300. {/*</Tooltip>*/}
  301. </Descriptions.Item>
  302. <Descriptions.Item label={'所属产品'}>
  303. <PermissionButton
  304. type={'link'}
  305. size={'small'}
  306. tooltip={{
  307. title: InstanceModel.detail?.productName,
  308. placement: 'topLeft',
  309. }}
  310. isPermission={!!getMenuPathByCode(MENUS_CODE['device/Product'])}
  311. onClick={() => {
  312. const url = getMenuPathByParams(
  313. MENUS_CODE['device/Product/Detail'],
  314. InstanceModel.detail?.productId,
  315. );
  316. if (url) {
  317. history.replace(url);
  318. }
  319. }}
  320. >
  321. <Ellipsis
  322. title={InstanceModel.detail?.productName}
  323. tooltip={{ placement: 'topLeft' }}
  324. style={{ maxWidth: 250 }}
  325. limitWidth={250}
  326. />
  327. {/*<div className="ellipsis" style={{ width: 250 }}>*/}
  328. {/* {InstanceModel.detail?.productName}*/}
  329. {/*</div>*/}
  330. </PermissionButton>
  331. </Descriptions.Item>
  332. </Descriptions>
  333. }
  334. title={
  335. <div style={{ display: 'flex', alignItems: 'center' }}>
  336. {/*<Tooltip placement="topLeft" title={InstanceModel.detail?.name}>*/}
  337. {/* <div className="ellipsis" style={{ maxWidth: 250 }}>*/}
  338. {/* {InstanceModel.detail?.name}*/}
  339. {/* </div>*/}
  340. {/*</Tooltip>*/}
  341. <Ellipsis
  342. title={InstanceModel.detail?.name}
  343. tooltip={{ placement: 'topLeft' }}
  344. style={{ maxWidth: 250 }}
  345. limitWidth={250}
  346. />
  347. <Divider type="vertical" />
  348. <Space>
  349. {deviceStatus.get(InstanceModel.detail?.state?.value)}
  350. {InstanceModel.detail?.state?.value === 'notActive' && (
  351. <PermissionButton
  352. type={'link'}
  353. key={'state'}
  354. popConfirm={{
  355. title: '确认启用设备',
  356. onConfirm: async () => {
  357. const resp = await service.deployDevice(params.id);
  358. if (resp.status === 200) {
  359. onlyMessage(
  360. intl.formatMessage({
  361. id: 'pages.data.option.success',
  362. defaultMessage: '操作成功!',
  363. }),
  364. );
  365. getDetail(params.id);
  366. }
  367. },
  368. }}
  369. isPermission={permission.action}
  370. tooltip={{
  371. title: '启用设备',
  372. }}
  373. >
  374. 启用设备
  375. </PermissionButton>
  376. )}
  377. {InstanceModel.detail?.state?.value === 'online' && (
  378. <PermissionButton
  379. type={'link'}
  380. key={'state'}
  381. popConfirm={{
  382. title: '确认断开连接',
  383. onConfirm: async () => {
  384. const resp = await service.disconnectDevice(params.id);
  385. if (resp.status === 200) {
  386. onlyMessage(
  387. intl.formatMessage({
  388. id: 'pages.data.option.success',
  389. defaultMessage: '操作成功!',
  390. }),
  391. );
  392. getDetail(params.id);
  393. }
  394. },
  395. }}
  396. isPermission={permission.action}
  397. tooltip={{
  398. title: '断开连接',
  399. }}
  400. >
  401. 断开连接
  402. </PermissionButton>
  403. )}
  404. {InstanceModel.detail?.accessProvider === 'child-device' &&
  405. InstanceModel.detail?.state?.value === 'offline' ? (
  406. <div>
  407. <Tooltip
  408. placement="bottom"
  409. title={
  410. InstanceModel.detail?.features?.find((item) => item.id === 'selfManageState')
  411. ? '该设备的在线状态与父设备(网关设备)保持一致'
  412. : '该设备在线状态由设备自身运行状态决定,不继承父设备(网关设备)的在线状态'
  413. }
  414. >
  415. <QuestionCircleOutlined style={{ fontSize: 14, marginRight: 5 }} />
  416. </Tooltip>
  417. </div>
  418. ) : (
  419. ''
  420. )}
  421. </Space>
  422. </div>
  423. }
  424. extra={[
  425. // <Button key="2">
  426. // {intl.formatMessage({
  427. // id: 'pages.device.productDetail.disable',
  428. // defaultMessage: '停用',
  429. // })}
  430. // </Button>,
  431. // <Button key="1" type="primary">
  432. // {intl.formatMessage({
  433. // id: 'pages.device.productDetail.setting',
  434. // defaultMessage: '应用配置',
  435. // })}
  436. // </Button>,
  437. <img
  438. style={{ marginRight: 20, cursor: 'pointer' }}
  439. src={require('/public/images/device/button.png')}
  440. onClick={() => {
  441. getDetail(params.id);
  442. service.getConfigMetadata(params.id).then((config) => {
  443. if (config.status === 200) {
  444. InstanceModel.config = config?.result || [];
  445. message.success('操作成功!');
  446. }
  447. });
  448. }}
  449. />,
  450. // <SyncOutlined
  451. // onClick={() => {
  452. // getDetail(params.id);
  453. // service.getConfigMetadata(params.id).then((config) => {
  454. // if (config.status === 200) {
  455. // InstanceModel.config = config?.result || [];
  456. // message.success('操作成功!');
  457. // }
  458. // });
  459. // }}
  460. // style={{ fontSize: 20, marginRight: 20 }}
  461. // key="1"
  462. // />,
  463. ]}
  464. >
  465. {list.find((k) => k.key === InstanceModel.active)?.component}
  466. </PageContainer>
  467. );
  468. });
  469. export default InstanceDetail;