index.tsx 16 KB

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