index.vue 39 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214
  1. <template>
  2. <wrapper
  3. class="c-mesh"
  4. fill
  5. margin
  6. padding
  7. background
  8. >
  9. <div class="l-flex__none l-flex--row c-mesh__header c-sibling-item--v">
  10. <i
  11. class="l-flex__none c-sibling-item o-icon medium o-icon--hover el-icon-arrow-left u-bold u-pointer"
  12. @click="onBack"
  13. />
  14. <div class="l-flex__auto l-flex--row c-sibling-item near u-color--black u-font-size--md u-bold u-ellipsis">
  15. {{ name }}
  16. </div>
  17. </div>
  18. <div
  19. v-loading="loading"
  20. class="l-flex__fill l-flex--col c-sibling-item--v"
  21. >
  22. <template v-if="!loading">
  23. <warning
  24. v-if="error"
  25. class="l-flex__auto"
  26. @click="getMesh"
  27. />
  28. <template v-else>
  29. <div class="l-flex__none l-flex--row c-sibling-item--v u-line-height">
  30. <div class="c-sibling-item u-font-size--md u-ellipsis">节点</div>
  31. <i
  32. class="l-flex__none c-sibling-item el-icon-circle-plus-outline u-font-size--lg u-color--blue u-bold has-active"
  33. @click.stop="onAddNode"
  34. />
  35. <i
  36. class="l-flex__none c-sibling-item el-icon-refresh-left u-font-size--lg u-color--blue u-bold has-active"
  37. @click.stop="getMesh"
  38. />
  39. </div>
  40. <div
  41. v-if="!hasNodes"
  42. class="has-padding u-text--center u-bold"
  43. >
  44. 暂无结点,请添加节点
  45. </div>
  46. <div
  47. ref="canvas"
  48. class="l-flex__fill"
  49. style="z-index: 0;"
  50. />
  51. <confirm-dialog
  52. ref="nodeDialog"
  53. :title="nodeDialogTitle"
  54. @confirm="onSubmit"
  55. >
  56. <template #default>
  57. <div class="c-grid-form u-align-self--center">
  58. <span class="c-grid-form__label u-required">类型</span>
  59. <schema-select
  60. v-model="item.nodeType"
  61. :schema="thirdPartyDeviceSelectSchema"
  62. />
  63. <span class="c-grid-form__label">别名</span>
  64. <el-input
  65. v-model.trim="item.name"
  66. placeholder="最多30个字符"
  67. maxlength="30"
  68. clearable
  69. />
  70. <template v-if="needPort">
  71. <span class="c-grid-form__label u-required">端口</span>
  72. <div
  73. class="has-info"
  74. data-info="若设备上的端口从1开始,填入时需减1处理"
  75. >
  76. <el-input-number
  77. v-model="item.port"
  78. controls-position="right"
  79. :min="0"
  80. step-strictly
  81. />
  82. </div>
  83. </template>
  84. </div>
  85. </template>
  86. </confirm-dialog>
  87. <confirm-dialog
  88. ref="nodeEditDialog"
  89. title="修改节点"
  90. @confirm="onSubmit"
  91. >
  92. <div class="c-grid-form u-align-self--center">
  93. <span class="c-grid-form__label">别名</span>
  94. <el-input
  95. v-model.trim="item.name"
  96. placeholder="最多30个字符"
  97. maxlength="30"
  98. clearable
  99. />
  100. </div>
  101. </confirm-dialog>
  102. <confirm-dialog
  103. ref="portEditDialog"
  104. title="端口设置"
  105. @confirm="onSubmitPort"
  106. >
  107. <template #default>
  108. <div class="c-grid-form auto u-align-self--center">
  109. <span class="c-grid-form__label u-required">端口</span>
  110. <div
  111. class="has-info u-width--md"
  112. data-info="若设备上的端口从1开始,填入时需减1处理"
  113. >
  114. <el-input-number
  115. v-model="item.port"
  116. controls-position="right"
  117. :min="0"
  118. step-strictly
  119. />
  120. </div>
  121. </div>
  122. </template>
  123. </confirm-dialog>
  124. <radio-table-dialog
  125. ref="instanceDialog"
  126. title="设备绑定"
  127. size="lg"
  128. message="请选择设备"
  129. :schema="thirdPartySchema"
  130. @confirm="onSubmitBind"
  131. />
  132. <component
  133. :is="thirdPartyComponent"
  134. v-if="thirdPartyComponent"
  135. ref="thirdPartyConfigDialog"
  136. @confirm="onConfirmThirdPaty"
  137. />
  138. <el-card
  139. v-show="menuVisible"
  140. ref="card"
  141. class="c-contentmenu"
  142. @click.native.stop
  143. >
  144. <template v-if="isNode">
  145. <div
  146. v-if="canHasFollow"
  147. class="l-flex--row c-contentmenu__item has-active"
  148. @click="onAddChildNode"
  149. >
  150. <i class="c-sibling-item el-icon-circle-plus-outline" />
  151. <span class="c-sibling-item">新增子节点</span>
  152. </div>
  153. <div
  154. v-if="canHasAhead"
  155. class="l-flex--row c-contentmenu__item has-active"
  156. @click="onAddParentNode"
  157. >
  158. <i class="c-sibling-item el-icon-circle-plus-outline" />
  159. <span class="c-sibling-item">新增父节点</span>
  160. </div>
  161. <div
  162. v-if="canLink"
  163. class="l-flex--row c-contentmenu__item has-active"
  164. @click="onLinkTo"
  165. >
  166. <i class="c-sibling-item el-icon-edit" />
  167. <span class="c-sibling-item">连接至</span>
  168. </div>
  169. <div
  170. class="l-flex--row c-contentmenu__item has-active"
  171. @click="onEditNode"
  172. >
  173. <i class="c-sibling-item el-icon-edit" />
  174. <span class="c-sibling-item">修改别名</span>
  175. </div>
  176. <div
  177. class="l-flex--row c-contentmenu__item has-active"
  178. @click="onToggleInstance"
  179. >
  180. <i class="c-sibling-item el-icon-edit" />
  181. <span class="c-sibling-item">{{ hasInstance ? '解绑' : '绑定' }}</span>
  182. </div>
  183. </template>
  184. <template v-else>
  185. <div
  186. v-if="hasPort"
  187. class="l-flex--row c-contentmenu__item has-active"
  188. @click="onEditPort"
  189. >
  190. <i class="c-sibling-item el-icon-edit" />
  191. <span class="c-sibling-item">端口</span>
  192. </div>
  193. </template>
  194. <div
  195. class="l-flex--row c-contentmenu__item has-active"
  196. @click="onDel"
  197. >
  198. <i class="c-sibling-item el-icon-remove-outline" />
  199. <span class="c-sibling-item">删除</span>
  200. </div>
  201. </el-card>
  202. </template>
  203. </template>
  204. </div>
  205. </wrapper>
  206. </template>
  207. <script>
  208. import {
  209. ThirdPartyDevice,
  210. ThirdPartyDeviceInfo,
  211. SendingCard,
  212. SendingCardFeatures,
  213. ReceivingCard,
  214. ReceivingCardFeatures,
  215. ThirdPartyToSensorMap,
  216. SensorToThirdPartyMap,
  217. ThirdPartyToCameraMap
  218. } from '@/constant'
  219. import {
  220. getMesh,
  221. addNodeToMesh,
  222. deleteNode,
  223. addEdgeToMesh,
  224. deleteEdge,
  225. updateEdgePort,
  226. updateNode,
  227. addNodeBeforeNode,
  228. addNodeAfterNode,
  229. bindInstanceToNode,
  230. releaseInstanceFormNode
  231. } from '@/api/mesh'
  232. import {
  233. getManufacturersByType,
  234. getGateways,
  235. getScreens,
  236. getSendingCards,
  237. getReceivingCards,
  238. getMultifunctionCards,
  239. getSensors,
  240. getPLCs,
  241. getCameras
  242. } from '@/api/external'
  243. import { getDevices } from '@/api/device'
  244. import * as echarts from 'echarts'
  245. import GatewayConfigDialog from '../components/GatewayConfigDialog.vue'
  246. import ScreenConfigDialog from '../components/ScreenConfigDialog.vue'
  247. import SendingCardConfigDialog from '../components/SendingCardConfigDialog.vue'
  248. import ReceivingCardConfigDialog from '../components/ReceivingCardConfigDialog.vue'
  249. import PlcConfigDialog from '../components/PlcConfigDialog.vue'
  250. import MultifunctionCardConfigDialog from '../components/MultifunctionCardConfigDialog.vue'
  251. import SensorConfigDialog from '../components/SensorConfigDialog.vue'
  252. import CameraConfigDialog from '../components/CameraConfigDialog.vue'
  253. const sensors = Object.values(SensorToThirdPartyMap)
  254. const PortThirdPartyDevices = [
  255. ThirdPartyDevice.PLC,
  256. ThirdPartyDevice.MULTI_FUNCTION_CARD,
  257. ...sensors
  258. ]
  259. const SoloThirdPartyDevices = [
  260. ThirdPartyDevice.RECEIVING_CARD,
  261. ThirdPartyDevice.SENDING_CARD,
  262. ThirdPartyDevice.SCREEN,
  263. ThirdPartyDevice.BOX
  264. ]
  265. const ThirdPartyDeviceOption = {
  266. 'all': Object.values(ThirdPartyDevice),
  267. follow: {
  268. [ThirdPartyDevice.GATEWAY]: [
  269. ThirdPartyDevice.PLC,
  270. ...sensors
  271. ],
  272. [ThirdPartyDevice.RECEIVING_CARD]: [
  273. ThirdPartyDevice.SCREEN
  274. ],
  275. [ThirdPartyDevice.SENDING_CARD]: [
  276. ThirdPartyDevice.RECEIVING_CARD,
  277. ThirdPartyDevice.SCREEN
  278. ],
  279. [ThirdPartyDevice.RECEIVING_CARD]: [
  280. ThirdPartyDevice.SCREEN
  281. ],
  282. [ThirdPartyDevice.BOX]: [
  283. ThirdPartyDevice.RECEIVING_CARD,
  284. ThirdPartyDevice.SENDING_CARD,
  285. ThirdPartyDevice.SCREEN,
  286. ThirdPartyDevice.PLC,
  287. ThirdPartyDevice.MULTI_FUNCTION_CARD,
  288. ...sensors
  289. ],
  290. [ThirdPartyDevice.MULTI_FUNCTION_CARD]: [
  291. ThirdPartyDevice.PLC,
  292. ...sensors
  293. ]
  294. },
  295. ahead: {
  296. [ThirdPartyDevice.RECEIVING_CARD]: [
  297. ThirdPartyDevice.SENDING_CARD,
  298. ThirdPartyDevice.BOX
  299. ],
  300. [ThirdPartyDevice.SENDING_CARD]: [
  301. ThirdPartyDevice.BOX
  302. ],
  303. [ThirdPartyDevice.SCREEN]: [
  304. ThirdPartyDevice.RECEIVING_CARD,
  305. ThirdPartyDevice.BOX
  306. ],
  307. [ThirdPartyDevice.PLC]: [
  308. ThirdPartyDevice.GATEWAY,
  309. ThirdPartyDevice.BOX,
  310. ThirdPartyDevice.MULTI_FUNCTION_CARD
  311. ],
  312. [ThirdPartyDevice.MULTI_FUNCTION_CARD]: [
  313. ThirdPartyDevice.BOX
  314. ],
  315. [ThirdPartyDevice.SMOKE_SENSOR]: [
  316. ThirdPartyDevice.GATEWAY,
  317. ThirdPartyDevice.BOX,
  318. ThirdPartyDevice.MULTI_FUNCTION_CARD
  319. ],
  320. [ThirdPartyDevice.TEMPERATURE_SENSOR]: [
  321. ThirdPartyDevice.GATEWAY,
  322. ThirdPartyDevice.BOX,
  323. ThirdPartyDevice.MULTI_FUNCTION_CARD
  324. ],
  325. [ThirdPartyDevice.LIGHT_SENSOR]: [
  326. ThirdPartyDevice.GATEWAY,
  327. ThirdPartyDevice.BOX,
  328. ThirdPartyDevice.MULTI_FUNCTION_CARD
  329. ],
  330. [ThirdPartyDevice.FLOODING_SENSOR]: [
  331. ThirdPartyDevice.GATEWAY,
  332. ThirdPartyDevice.BOX,
  333. ThirdPartyDevice.MULTI_FUNCTION_CARD
  334. ],
  335. [ThirdPartyDevice.TRANSLOCATION_SENSOR]: [
  336. ThirdPartyDevice.GATEWAY,
  337. ThirdPartyDevice.BOX,
  338. ThirdPartyDevice.MULTI_FUNCTION_CARD
  339. ]
  340. }
  341. }
  342. export default {
  343. name: 'Mesh',
  344. components: {
  345. GatewayConfigDialog,
  346. ScreenConfigDialog,
  347. SendingCardConfigDialog,
  348. ReceivingCardConfigDialog,
  349. PlcConfigDialog,
  350. MultifunctionCardConfigDialog,
  351. SensorConfigDialog,
  352. CameraConfigDialog
  353. },
  354. data () {
  355. return {
  356. loading: true,
  357. error: false,
  358. mesh: null,
  359. defaultProps: { label: 'name' },
  360. thirdPartyDeviceSelectSchema: null,
  361. item: {},
  362. nodes: [],
  363. menuVisible: false,
  364. clickTargetType: '',
  365. targetItem: null
  366. }
  367. },
  368. computed: {
  369. meshId () {
  370. return this.$route.params.id
  371. },
  372. name () {
  373. return this.mesh?.name
  374. },
  375. hasNodes () {
  376. return this.nodes.length > 0
  377. },
  378. isNode () {
  379. return this.hasNodes && this.clickTargetType === 'node'
  380. },
  381. hasInstance () {
  382. return this.targetItem?.instanceId
  383. },
  384. nodeType () {
  385. return this.targetItem?.nodeType
  386. },
  387. canHasAhead () {
  388. return !!ThirdPartyDeviceOption.ahead[this.nodeType]
  389. },
  390. canHasFollow () {
  391. return !!ThirdPartyDeviceOption.follow[this.nodeType]
  392. },
  393. canLink () {
  394. if (this.isNode && this.canHasFollow) {
  395. const options = ThirdPartyDeviceOption.follow[this.nodeType]
  396. return options
  397. ? this.nodes.some(({ nodeType }) => options.includes(nodeType))
  398. : false
  399. }
  400. return false
  401. },
  402. hasPort () {
  403. return this.targetItem?.port >= 0
  404. },
  405. nodeSelectSchema () {
  406. return {
  407. options: this.nodes.map(({ id, name }) => {
  408. return {
  409. value: id,
  410. label: name
  411. }
  412. })
  413. }
  414. },
  415. nodeDialogTitle () {
  416. const { action } = this.item
  417. switch (action) {
  418. case 'addParentNode':
  419. return '新增父节点'
  420. case 'addChildNode':
  421. return '新增子节点'
  422. default:
  423. return '新增节点'
  424. }
  425. },
  426. needPort () {
  427. const { action } = this.item
  428. switch (action) {
  429. case 'addParentNode':
  430. return PortThirdPartyDevices.includes(this.targetItem.nodeType)
  431. case 'addChildNode':
  432. return PortThirdPartyDevices.includes(this.item.nodeType)
  433. default:
  434. return false
  435. }
  436. },
  437. thirdPartyType () {
  438. return this.targetItem?.nodeType
  439. },
  440. thirdPartyComponent () {
  441. switch (this.thirdPartyType) {
  442. case ThirdPartyDevice.GATEWAY:
  443. return 'GatewayConfigDialog'
  444. case ThirdPartyDevice.SCREEN:
  445. return 'ScreenConfigDialog'
  446. case ThirdPartyDevice.SENDING_CARD:
  447. return 'SendingCardConfigDialog'
  448. case ThirdPartyDevice.RECEIVING_CARD:
  449. return 'ReceivingCardConfigDialog'
  450. case ThirdPartyDevice.PLC:
  451. return 'PlcConfigDialog'
  452. case ThirdPartyDevice.MULTI_FUNCTION_CARD:
  453. return 'MultifunctionCardConfigDialog'
  454. case ThirdPartyDevice.SMOKE_SENSOR:
  455. case ThirdPartyDevice.TEMPERATURE_SENSOR:
  456. case ThirdPartyDevice.LIGHT_SENSOR:
  457. case ThirdPartyDevice.FLOODING_SENSOR:
  458. case ThirdPartyDevice.TRANSLOCATION_SENSOR:
  459. return 'SensorConfigDialog'
  460. case ThirdPartyDevice.LED_CAMERA:
  461. case ThirdPartyDevice.TRAFFIC_CAMERA:
  462. return 'CameraConfigDialog'
  463. default:
  464. return null
  465. }
  466. },
  467. thirdPartySchema () {
  468. switch (this.thirdPartyType) {
  469. case ThirdPartyDevice.GATEWAY:
  470. return {
  471. list: getGateways,
  472. condition: { boundFlag: 0 },
  473. buttons: [
  474. { type: 'add', on: this.onAddThirdParty }
  475. ],
  476. filters: [
  477. { key: 'manufacturerKey', type: 'select', placeholder: '厂商', remote: this.getManufacturersByType, value: 'manufacturerKey', label: 'manufacturerName' },
  478. { key: 'identifier', type: 'search', placeholder: '唯一标识' }
  479. ],
  480. cols: [
  481. { prop: 'manufacturerName', label: '厂商' },
  482. { prop: 'model', label: '型号' },
  483. { prop: 'ip', label: '服务器地址' },
  484. { prop: 'identifier', label: '唯一标识' }
  485. ]
  486. }
  487. case ThirdPartyDevice.SCREEN:
  488. return {
  489. list: getScreens,
  490. condition: { boundFlag: 0 },
  491. buttons: [
  492. { type: 'add', on: this.onAddThirdParty }
  493. ],
  494. filters: [
  495. { key: 'manufacturerKey', type: 'select', placeholder: '厂商', remote: this.getManufacturersByType, value: 'manufacturerKey', label: 'manufacturerName' },
  496. { key: 'identifier', type: 'search', placeholder: '唯一标识' }
  497. ],
  498. cols: [
  499. { prop: 'manufacturerName', label: '厂商' },
  500. { prop: 'model', label: '型号' },
  501. { prop: 'identifier', label: '唯一标识' },
  502. { prop: 'measure', label: '屏宽高(m²)' },
  503. { prop: 'pitch', label: '点间距(mm)' },
  504. { prop: 'resolutionRatio', label: '分辨率' }
  505. ]
  506. }
  507. case ThirdPartyDevice.SENDING_CARD:
  508. return {
  509. list: getSendingCards,
  510. condition: { boundFlag: 0 },
  511. buttons: [
  512. { type: 'add', on: this.onAddThirdParty }
  513. ],
  514. filters: [
  515. { key: 'manufacturerKey', type: 'select', placeholder: '厂商', remote: this.getManufacturersByType, value: 'manufacturerKey', label: 'manufacturerName' },
  516. { key: 'identifier', type: 'search', placeholder: '唯一标识' }
  517. ],
  518. cols: [
  519. { prop: 'manufacturerName', label: '厂商' },
  520. { prop: 'model', label: '型号' },
  521. { prop: 'identifier', label: '唯一标识' },
  522. { label: '特性', type: 'tag', render: ({ flag }) => SendingCardFeatures.map(({ key, label }) => {
  523. return { type: flag & SendingCard[key] ? 'success' : 'danger', label }
  524. }), width: 364 }
  525. ]
  526. }
  527. case ThirdPartyDevice.RECEIVING_CARD:
  528. return {
  529. list: getReceivingCards,
  530. condition: { boundFlag: 0 },
  531. buttons: [
  532. { type: 'add', on: this.onAddThirdParty }
  533. ],
  534. filters: [
  535. { key: 'manufacturerKey', type: 'select', placeholder: '厂商', remote: this.getManufacturersByType, value: 'manufacturerKey', label: 'manufacturerName' },
  536. { key: 'identifier', type: 'search', placeholder: '唯一标识' }
  537. ],
  538. cols: [
  539. { prop: 'manufacturerName', label: '厂商' },
  540. { prop: 'model', label: '型号' },
  541. { prop: 'identifier', label: '唯一标识' },
  542. { label: '特性', type: 'tag', render: ({ flag }) => ReceivingCardFeatures.map(({ key, label }) => {
  543. return { type: flag & ReceivingCard[key] ? 'success' : 'danger', label }
  544. }), width: 364 }
  545. ]
  546. }
  547. case ThirdPartyDevice.PLC:
  548. return {
  549. list: getPLCs,
  550. condition: { boundFlag: 0 },
  551. buttons: [
  552. { type: 'add', on: this.onAddThirdParty }
  553. ],
  554. filters: [
  555. { key: 'manufacturerKey', type: 'select', placeholder: '厂商', remote: this.getManufacturersByType, value: 'manufacturerKey', label: 'manufacturerName' },
  556. { key: 'identifier', type: 'search', placeholder: '唯一标识' }
  557. ],
  558. cols: [
  559. { prop: 'manufacturerName', label: '厂商' },
  560. { prop: 'model', label: '型号' },
  561. { prop: 'identifier', label: '唯一标识' },
  562. { prop: 'portCount', label: '端口数' }
  563. ]
  564. }
  565. case ThirdPartyDevice.BOX:
  566. return {
  567. list: getDevices,
  568. condition: { boundFlag: 0 },
  569. filters: [
  570. { key: 'serialNumber', type: 'search', placeholder: '序列号' },
  571. { key: 'mac', type: 'search', placeholder: 'MAC' },
  572. { key: 'name', type: 'search', placeholder: '设备名称' }
  573. ],
  574. cols: [
  575. { prop: 'name', label: '设备名称' },
  576. { prop: 'serialNumber', label: '序列号' },
  577. { prop: 'mac', label: 'MAC' }
  578. ]
  579. }
  580. case ThirdPartyDevice.MULTI_FUNCTION_CARD:
  581. return {
  582. list: getMultifunctionCards,
  583. condition: { boundFlag: 0 },
  584. buttons: [
  585. { type: 'add', on: this.onAddThirdParty }
  586. ],
  587. filters: [
  588. { key: 'manufacturerKey', type: 'select', placeholder: '厂商', remote: this.getManufacturersByType, value: 'manufacturerKey', label: 'manufacturerName' },
  589. { key: 'identifier', type: 'search', placeholder: '唯一标识' }
  590. ],
  591. cols: [
  592. { prop: 'manufacturerName', label: '厂商' },
  593. { prop: 'model', label: '型号' },
  594. { prop: 'identifier', label: '唯一标识' },
  595. { prop: 'portCount', label: '端口数', align: 'center' },
  596. { prop: 'rs485Count', label: '485接口数', align: 'center' }
  597. ]
  598. }
  599. case ThirdPartyDevice.SMOKE_SENSOR:
  600. case ThirdPartyDevice.TEMPERATURE_SENSOR:
  601. case ThirdPartyDevice.LIGHT_SENSOR:
  602. case ThirdPartyDevice.FLOODING_SENSOR:
  603. case ThirdPartyDevice.TRANSLOCATION_SENSOR:
  604. return {
  605. list: getSensors,
  606. condition: { sensorType: ThirdPartyToSensorMap[this.thirdPartyType], boundFlag: 0 },
  607. buttons: [
  608. { type: 'add', on: this.onAddThirdParty }
  609. ],
  610. filters: [
  611. { key: 'manufacturerKey', type: 'select', placeholder: '厂商', remote: this.getManufacturersByType, value: 'manufacturerKey', label: 'manufacturerName' },
  612. { key: 'identifier', type: 'search', placeholder: '唯一标识' }
  613. ],
  614. cols: [
  615. { prop: 'manufacturerName', label: '厂商' },
  616. { prop: 'model', label: '型号' },
  617. { prop: 'identifier', label: '唯一标识' }
  618. ]
  619. }
  620. case ThirdPartyDevice.LED_CAMERA:
  621. case ThirdPartyDevice.TRAFFIC_CAMERA:
  622. return {
  623. list: getCameras,
  624. condition: { boundFlag: 0, cameraType: ThirdPartyToCameraMap[this.thirdPartyType] },
  625. buttons: [
  626. { type: 'add', on: this.onAddThirdParty }
  627. ],
  628. filters: [
  629. { key: 'manufacturerKey', type: 'select', placeholder: '厂商', remote: this.getManufacturersByType, value: 'manufacturerKey', label: 'manufacturerName' },
  630. { key: 'identifier', type: 'search', placeholder: '唯一标识' },
  631. { key: 'remark', type: 'search', placeholder: '备注' }
  632. ],
  633. cols: [
  634. { prop: 'manufacturerName', label: '厂商' },
  635. { prop: 'model', label: '型号' },
  636. { prop: 'identifier', label: '唯一标识' },
  637. { prop: 'remark', label: '备注' }
  638. ]
  639. }
  640. default:
  641. return {
  642. list: () => Promise.resolve({ data: [] }),
  643. cols: [
  644. { label: '唯一标识' }
  645. ]
  646. }
  647. }
  648. }
  649. },
  650. watch: {
  651. meshId: {
  652. handler () {
  653. this.getMesh()
  654. },
  655. immediate: true
  656. }
  657. },
  658. beforeDestroy () {
  659. this.disposeChart()
  660. },
  661. methods: {
  662. getManufacturersByType () {
  663. return getManufacturersByType(this.thirdPartyType)
  664. },
  665. onAddThirdParty () {
  666. let type = null
  667. switch (this.thirdPartyType) {
  668. case ThirdPartyDevice.SMOKE_SENSOR:
  669. case ThirdPartyDevice.TEMPERATURE_SENSOR:
  670. case ThirdPartyDevice.LIGHT_SENSOR:
  671. case ThirdPartyDevice.FLOODING_SENSOR:
  672. case ThirdPartyDevice.TRANSLOCATION_SENSOR:
  673. type = ThirdPartyToSensorMap[this.thirdPartyType]
  674. break
  675. case ThirdPartyDevice.LED_CAMERA:
  676. case ThirdPartyDevice.TRAFFIC_CAMERA:
  677. type = ThirdPartyToCameraMap[this.thirdPartyType]
  678. break
  679. default:
  680. break
  681. }
  682. this.$refs.thirdPartyConfigDialog.show(type)
  683. },
  684. onConfirmThirdPaty ({ manufacturerKey, identifier, remark }) {
  685. this.$refs.instanceDialog.getTable().resetCondition({
  686. manufacturerKey,
  687. identifier,
  688. remark
  689. })
  690. },
  691. collectNodes (nodes) {
  692. this.$nodeMap = {}
  693. if (nodes) {
  694. nodes.forEach((node, index) => {
  695. this.$nodeMap[node.id] = index
  696. })
  697. }
  698. return nodes || []
  699. },
  700. onBack () {
  701. this.$router.replace({
  702. name: 'mesh-list'
  703. })
  704. },
  705. disposeChart () {
  706. if (this.mesh) {
  707. this.mesh = null
  708. this.nodes = []
  709. this.edges = []
  710. this.$chart?.dispose()
  711. this.$chart = null
  712. this.closeContentMenu()
  713. }
  714. this.$nodeMap = {}
  715. this.$waitTarget = false
  716. },
  717. getMesh () {
  718. this.disposeChart()
  719. this.loading = true
  720. this.error = false
  721. getMesh(this.meshId).then(
  722. ({ data }) => {
  723. if (data) {
  724. this.mesh = data.mesh
  725. this.edges = data.bondList || []
  726. this.nodes = this.collectNodes(data.nodeList)
  727. } else {
  728. this.$message({
  729. type: 'warning',
  730. message: '数据不存在'
  731. })
  732. this.onBack()
  733. }
  734. },
  735. () => {
  736. this.error = true
  737. }
  738. ).finally(() => {
  739. this.loading = false
  740. if (this.nodes?.length) {
  741. this.$nextTick(this.onDraw)
  742. }
  743. })
  744. },
  745. onDraw () {
  746. if (!this.$chart) {
  747. this.$chart = echarts.init(this.$refs.canvas)
  748. this.$chart.on('click', this.onClick)
  749. }
  750. const options = this.$waitTarget && ThirdPartyDeviceOption.follow[this.targetItem.nodeType]
  751. this.$chart.setOption({
  752. // title: {
  753. // text: 'Basic Graph'
  754. // },
  755. animationDurationUpdate: 1500,
  756. animationEasingUpdate: 'quinticInOut',
  757. tooltip: {},
  758. series: [
  759. {
  760. type: 'graph',
  761. // roam: true,
  762. layout: 'circular',
  763. // layout: 'force',
  764. // force: {
  765. // edgeLength: 100
  766. // },
  767. label: {
  768. show: true,
  769. position: 'top'
  770. },
  771. itemStyle: {
  772. color ({ data, color }) {
  773. return data.proxy.instanceId ? '#67c23a' : color
  774. }
  775. },
  776. edgeSymbol: ['none', 'arrow'], // circle
  777. edgeSymbolSize: 8,
  778. // edgeLabel: {
  779. // fontSize: 20
  780. // },
  781. lineStyle: {
  782. // color: '#ff0000',
  783. width: 2,
  784. opacity: 0.9,
  785. curveness: 0
  786. },
  787. nodes: this.nodes.map(node => {
  788. const { id, name, nodeType } = node
  789. const canClick = options ? options.includes(nodeType) : true
  790. return {
  791. proxy: node,
  792. id,
  793. name,
  794. tooltip: {
  795. formatter: this.formatterNode
  796. },
  797. canClick,
  798. emphasis: {
  799. disabled: !canClick
  800. },
  801. symbolSize: canClick ? 24 : 12
  802. }
  803. }),
  804. edges: this.edges.map(edge => {
  805. const { preNodeId, nextNodeId, port } = edge
  806. return {
  807. proxy: edge,
  808. value: port,
  809. source: this.$nodeMap[preNodeId],
  810. target: this.$nodeMap[nextNodeId],
  811. label: {
  812. show: port >= 0,
  813. // position: 'start',
  814. formatter: '{c}'
  815. }
  816. }
  817. })
  818. }
  819. ]
  820. })
  821. },
  822. formatterNode ({ marker, data: { proxy: { nodeType, instance } } }) {
  823. let tooltip = `${marker} ${ThirdPartyDeviceInfo[nodeType]}`
  824. if (instance) {
  825. switch (nodeType) {
  826. case ThirdPartyDevice.SENDING_CARD:
  827. case ThirdPartyDevice.SMOKE_SENSOR:
  828. case ThirdPartyDevice.TEMPERATURE_SENSOR:
  829. case ThirdPartyDevice.LIGHT_SENSOR:
  830. case ThirdPartyDevice.FLOODING_SENSOR:
  831. case ThirdPartyDevice.TRANSLOCATION_SENSOR:
  832. tooltip += `<br/>唯一标识:${instance.identifier}<br/>厂商:${instance.manufacturerName}<br/>型号:${instance.model}`
  833. break
  834. case ThirdPartyDevice.RECEIVING_CARD:
  835. tooltip += `<br/>唯一标识:${instance.identifier}<br/>厂商:${instance.manufacturerName}<br/>型号:${instance.model}<br/>规格:${instance.rowCount} x ${instance.colCount}`
  836. break
  837. case ThirdPartyDevice.GATEWAY:
  838. tooltip += `<br/>唯一标识:${instance.identifier}<br/>厂商:${instance.manufacturerName}<br/>型号:${instance.model}<br/>服务器:${instance.ip}`
  839. break
  840. case ThirdPartyDevice.SCREEN:
  841. tooltip += `<br/>唯一标识:${instance.identifier}<br/>厂商:${instance.manufacturerName}<br/>型号:${instance.model}<br/>规格:${instance.measure}m² ${instance.pitch}mm`
  842. break
  843. case ThirdPartyDevice.PLC:
  844. tooltip += `<br/>唯一标识:${instance.identifier}<br/>厂商:${instance.manufacturerName}<br/>型号:${instance.model}<br/>端口数:${instance.portCount}`
  845. break
  846. case ThirdPartyDevice.BOX:
  847. tooltip += `<br/>设备名称:${instance.name}<br/>序列号:${instance.serialNumber}<br/>MAC:${instance.mac}`
  848. break
  849. case ThirdPartyDevice.MULTI_FUNCTION_CARD:
  850. tooltip += `<br/>唯一标识:${instance.identifier}<br/>厂商:${instance.manufacturerName}<br/>型号:${instance.model}<br/>端口数:${instance.portCount}<br/>485接口数:${instance.rs485Count}`
  851. break
  852. case ThirdPartyDevice.LED_CAMERA:
  853. case ThirdPartyDevice.TRAFFIC_CAMERA:
  854. tooltip += `<br/>唯一标识:${instance.identifier}<br/>厂商:${instance.manufacturerName}<br/>型号:${instance.model}<br/>备注:${instance.remark}`
  855. break
  856. default:
  857. break
  858. }
  859. }
  860. return tooltip
  861. },
  862. onClick (eventProxy) {
  863. console.log('click', eventProxy)
  864. const targetItem = eventProxy.data.proxy
  865. const targetType = eventProxy.dataType
  866. if (this.$waitTarget) {
  867. this.onChangeLinkStatus(false)
  868. if (targetType === 'node') {
  869. if (eventProxy.data.canClick) {
  870. this.addEdge(this.targetItem, targetItem)
  871. return
  872. } else if (this.targetItem.id === targetItem.id) {
  873. this.$message({
  874. type: 'warning',
  875. message: '节点不能连接自身'
  876. })
  877. } else {
  878. this.$message({
  879. type: 'warning',
  880. message: '无法连接至该节点'
  881. })
  882. }
  883. return
  884. }
  885. }
  886. this.clickTargetType = targetType
  887. if (this.isNode) {
  888. this.targetItem = this.nodes[this.$nodeMap[targetItem.id]]
  889. } else {
  890. const id = targetItem.nodeBondId
  891. this.targetItem = this.edges.find(edge => edge.nodeBondId === id)
  892. }
  893. const event = eventProxy.event.event
  894. this.$refs.card.$el.style.left = `${Math.min(event.clientX, document.body.clientWidth - 160)}px`
  895. this.$refs.card.$el.style.top = `${Math.min(event.clientY, document.body.clientHeight - 240)}px`
  896. event.stopPropagation()
  897. if (!this.menuVisible) {
  898. this.menuVisible = true
  899. document.addEventListener('click', this.closeContentMenu)
  900. }
  901. },
  902. closeContentMenu () {
  903. if (this.menuVisible) {
  904. this.menuVisible = false
  905. document.removeEventListener('click', this.closeContentMenu)
  906. }
  907. },
  908. createOptions (action) {
  909. let arr = null
  910. switch (action) {
  911. case 'create':
  912. arr = ThirdPartyDeviceOption.all
  913. break
  914. case 'addChildNode':
  915. arr = ThirdPartyDeviceOption.follow[this.targetItem.nodeType]
  916. break
  917. case 'addParentNode':
  918. arr = ThirdPartyDeviceOption.ahead[this.targetItem.nodeType]
  919. break
  920. default:
  921. break
  922. }
  923. return arr?.length
  924. ? arr.map(key => {
  925. return {
  926. value: key,
  927. label: ThirdPartyDeviceInfo[key]
  928. }
  929. })
  930. : null
  931. },
  932. showNodeDialog (action) {
  933. const options = this.createOptions(action)
  934. if (!options) {
  935. this.$message({
  936. type: 'warning',
  937. message: '暂未能满足执行操作的条件'
  938. })
  939. return
  940. }
  941. this.item = {
  942. action,
  943. nodeType: options[0].value,
  944. name: '',
  945. port: 0
  946. }
  947. this.thirdPartyDeviceSelectSchema = { options }
  948. this.$refs.nodeDialog.show()
  949. },
  950. onAddNode () {
  951. this.closeContentMenu()
  952. this.showNodeDialog('create')
  953. },
  954. onAddChildNode () {
  955. this.closeContentMenu()
  956. this.showNodeDialog('addChildNode')
  957. },
  958. onAddParentNode () {
  959. this.closeContentMenu()
  960. this.showNodeDialog('addParentNode')
  961. },
  962. onEditNode () {
  963. this.closeContentMenu()
  964. this.item = {
  965. action: 'update',
  966. ...this.targetItem
  967. }
  968. this.$refs.nodeEditDialog.show()
  969. },
  970. isExist (type) {
  971. return SoloThirdPartyDevices.includes(type) && this.nodes.some(({ nodeType }) => nodeType === type)
  972. },
  973. addNode (item) {
  974. if (this.isExist(item.nodeType)) {
  975. this.$message({
  976. type: 'warning',
  977. message: `${ThirdPartyDeviceInfo[item.nodeType]}仅能存在一个`
  978. })
  979. return Promise.reject()
  980. }
  981. return addNodeToMesh(this.meshId, item).then(({ data }) => {
  982. this.nodes.push(data)
  983. this.collectNodes(this.nodes)
  984. this.onDraw()
  985. })
  986. },
  987. addNodeBeforeNode (item, port) {
  988. if (this.isExist(item.nodeType)) {
  989. this.$message({
  990. type: 'warning',
  991. message: `${ThirdPartyDeviceInfo[item.nodeType]}仅能存在一个`
  992. })
  993. return Promise.reject()
  994. }
  995. return addNodeBeforeNode(this.targetItem, item, port).then(({ data }) => {
  996. this.nodes.push(data.node)
  997. this.edges.push(data.bonds[0])
  998. this.collectNodes(this.nodes)
  999. this.onDraw()
  1000. })
  1001. },
  1002. addNodeAfterNode (item, port) {
  1003. if (this.isExist(item.nodeType)) {
  1004. this.$message({
  1005. type: 'warning',
  1006. message: `${ThirdPartyDeviceInfo[item.nodeType]}仅能存在一个`
  1007. })
  1008. return Promise.reject()
  1009. }
  1010. return addNodeAfterNode(this.targetItem, item, port).then(({ data }) => {
  1011. this.nodes.push(data.node)
  1012. this.edges.push(data.bonds[0])
  1013. this.collectNodes(this.nodes)
  1014. this.onDraw()
  1015. })
  1016. },
  1017. onSubmit (done) {
  1018. const { action, nodeType, name, port } = this.item
  1019. const item = {
  1020. name: name || ThirdPartyDeviceInfo[nodeType],
  1021. nodeType
  1022. }
  1023. switch (action) {
  1024. case 'addParentNode':
  1025. this.addNodeBeforeNode(item, this.needPort ? port : -1).then(done)
  1026. break
  1027. case 'addChildNode':
  1028. if (this.needPort && this.isPortUsed(this.targetItem, port)) {
  1029. return
  1030. }
  1031. this.addNodeAfterNode(item, this.needPort ? port : -1).then(done)
  1032. break
  1033. case 'update':
  1034. if (!name || this.targetItem.name === name) {
  1035. done()
  1036. } else {
  1037. updateNode({
  1038. id: this.targetItem.id,
  1039. ...item
  1040. }).then(() => {
  1041. done()
  1042. this.targetItem.name = name
  1043. this.onDraw()
  1044. })
  1045. }
  1046. break
  1047. default:
  1048. this.addNode(item).then(done)
  1049. break
  1050. }
  1051. },
  1052. isPortUsed ({ id }, targetPort) {
  1053. const edge = this.edges.find(({ preNodeId, port }) => preNodeId === id && port === targetPort)
  1054. if (edge) {
  1055. this.$message({
  1056. type: 'warning',
  1057. message: `端口已被${this.nodes[this.$nodeMap[edge.nextNodeId]].name}占用`
  1058. })
  1059. return true
  1060. }
  1061. return false
  1062. },
  1063. onSubmitPort (done) {
  1064. const { action, port } = this.item
  1065. switch (action) {
  1066. case 'edge':
  1067. if (this.isPortUsed(this.targetItem, port)) {
  1068. return
  1069. }
  1070. this.onAddEdge(this.targetItem, this.$targetNode, port).then(done)
  1071. break
  1072. case 'portEdit':
  1073. if (port === this.targetItem.port) {
  1074. done()
  1075. return
  1076. }
  1077. if (this.isPortUsed(this.nodes[this.$nodeMap[this.targetItem.preNodeId]], port)) {
  1078. return
  1079. }
  1080. updateEdgePort(this.targetItem, port).then(() => {
  1081. done()
  1082. this.targetItem.port = port
  1083. this.onDraw()
  1084. })
  1085. break
  1086. default:
  1087. done()
  1088. break
  1089. }
  1090. },
  1091. onLinkTo () {
  1092. this.closeContentMenu()
  1093. this.$message({
  1094. type: 'success',
  1095. message: '源节点已记录,请选择目标节点'
  1096. })
  1097. this.onChangeLinkStatus(true)
  1098. },
  1099. onChangeLinkStatus (bool) {
  1100. this.$waitTarget = bool
  1101. this.onDraw()
  1102. },
  1103. addEdge (source, target) {
  1104. const sourceId = source.id
  1105. const targetId = target.id
  1106. if (this.edges.some(({ preNodeId, nextNodeId }) => sourceId === preNodeId && targetId === nextNodeId)) {
  1107. this.$message({
  1108. type: 'warning',
  1109. message: '连接已存在,请勿重复连接'
  1110. })
  1111. return
  1112. }
  1113. if (PortThirdPartyDevices.includes(target.nodeType)) {
  1114. this.item = {
  1115. action: 'edge',
  1116. port: 0
  1117. }
  1118. this.$targetNode = target
  1119. this.$refs.portEditDialog.show()
  1120. } else {
  1121. this.onAddEdge(source, target, -1)
  1122. }
  1123. },
  1124. onAddEdge (source, target, port) {
  1125. return addEdgeToMesh(this.meshId, source, target, port).then(({ data }) => {
  1126. this.$targetNode = null
  1127. this.edges.push(data)
  1128. this.onDraw()
  1129. })
  1130. },
  1131. onDel () {
  1132. this.closeContentMenu()
  1133. if (this.isNode) {
  1134. deleteNode(this.targetItem).then(() => {
  1135. const id = this.targetItem.id
  1136. this.nodes.splice(this.nodes.findIndex(node => node.id === id), 1)
  1137. this.edges = this.edges.filter(({ preNodeId, nextNodeId }) => preNodeId !== id && nextNodeId !== id)
  1138. this.collectNodes(this.nodes)
  1139. this.onDraw()
  1140. })
  1141. } else {
  1142. deleteEdge(this.targetItem).then(() => {
  1143. const id = this.targetItem.nodeBondId
  1144. this.edges.splice(this.edges.findIndex(edge => edge.id === id), 1)
  1145. this.collectNodes(this.nodes)
  1146. this.onDraw()
  1147. })
  1148. }
  1149. },
  1150. onToggleInstance () {
  1151. this.closeContentMenu()
  1152. if (this.hasInstance) {
  1153. releaseInstanceFormNode(this.targetItem).then(() => {
  1154. this.targetItem.instanceId = null
  1155. this.targetItem.instance = null
  1156. this.onDraw()
  1157. })
  1158. } else {
  1159. this.$refs.instanceDialog.show()
  1160. }
  1161. },
  1162. onSubmitBind ({ value, done }) {
  1163. if (value.id === this.targetItem.instanceId) {
  1164. done()
  1165. return
  1166. }
  1167. bindInstanceToNode(this.targetItem, value).then(() => {
  1168. done()
  1169. this.targetItem.instanceId = value.id
  1170. this.targetItem.instance = value
  1171. this.onDraw()
  1172. })
  1173. },
  1174. onEditPort () {
  1175. this.closeContentMenu()
  1176. this.item = {
  1177. action: 'portEdit',
  1178. port: this.targetItem.port
  1179. }
  1180. this.$refs.portEditDialog.show()
  1181. }
  1182. }
  1183. }
  1184. </script>
  1185. <style lang="scss" scoped>
  1186. .c-mesh {
  1187. &__header {
  1188. height: $height--md;
  1189. border-bottom: 1px solid $border;
  1190. }
  1191. }
  1192. .c-contentmenu {
  1193. position: fixed;
  1194. width: 150px;
  1195. z-index: 99;
  1196. &__item {
  1197. & + & {
  1198. padding-top: 6px;
  1199. margin-top: 6px;
  1200. border-top: 1px solid $border;
  1201. }
  1202. }
  1203. }
  1204. </style>