MapData.vue 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  1. <template>
  2. <div>
  3. <div class="time-card">
  4. <i class="el-icon-time"></i>
  5. <div class="time-content">
  6. <div class="label">{{ $t('mes.lockCabinet.lastUpdate') }}</div>
  7. <div class="time">{{ updateTime }}</div>
  8. </div>
  9. </div>
  10. <div ref="container" style="width: 900px; height: 400px;margin: 0 auto"></div>
  11. <el-dialog :title="$t('mes.lockCabinet.exceptionInfo')" :visible.sync="dialogVisible" width="400px">
  12. <h4 style="text-align: center; font-weight: bolder">{{ errorInfo }}</h4>
  13. <div slot="footer" class="dialog-footer">
  14. <el-button v-no-more-click @click="cancel">{{ $t('common.close') }}</el-button>
  15. </div>
  16. </el-dialog>
  17. </div>
  18. </template>
  19. <script>
  20. import Konva from 'konva'
  21. import { getIsSystemAttributeByKey } from '@/api/system/configuration'
  22. import { getIsLockCabinetSlotsPage } from '@/api/mes/lockCabinet/slots'
  23. export default {
  24. data() {
  25. return {
  26. stage: null,
  27. layer: null,
  28. cachedResults: {},
  29. cachedImages: {},
  30. slotData: [],
  31. dialogVisible: false,
  32. errorInfo: '',
  33. updateTime: null
  34. }
  35. },
  36. mounted() {
  37. this.initKonva()
  38. this.getData()
  39. },
  40. methods: {
  41. cancel() {
  42. this.dialogVisible = false
  43. },
  44. async getData() {
  45. const data = {
  46. current: 1,
  47. size: -1,
  48. cabinetId: this.$route.query.cabinetId
  49. }
  50. try {
  51. const res = await getIsLockCabinetSlotsPage(data)
  52. this.updateTime = res.data.records[0].updateTime
  53. this.slotData = res.data?.records || []
  54. const icons = [
  55. 'icon.locker.normal',
  56. 'icon.locker.out',
  57. 'icon.padlock.normal',
  58. 'icon.padlock.out',
  59. 'icon.locker.exception'
  60. ]
  61. const results = await Promise.all(icons.map(key => getIsSystemAttributeByKey(key)))
  62. this.cachedResults = icons.reduce((map, key, idx) => {
  63. map[key] = results[idx].data?.sysAttrValue || ''
  64. return map
  65. }, {})
  66. await this.preloadImages()
  67. this.renderSlots()
  68. } catch (err) {
  69. console.error(this.$t('mes.lockCabinet.getDataFailed'), err)
  70. }
  71. },
  72. showErrorDialog(slot) {
  73. this.errorInfo = slot.remark || this.$t('mes.lockCabinet.unknownException')
  74. this.dialogVisible = true
  75. },
  76. initKonva() {
  77. this.stage = new Konva.Stage({
  78. container: this.$refs.container,
  79. width: 900,
  80. height: 800
  81. })
  82. this.layer = new Konva.Layer()
  83. this.stage.add(this.layer)
  84. },
  85. async preloadImages() {
  86. const urls = Object.values(this.cachedResults)
  87. const promises = urls.map(url => this.loadImageOnce(url))
  88. await Promise.all(promises)
  89. },
  90. async renderSlots() {
  91. this.layer.destroyChildren()
  92. const grouped = {}
  93. for (const slot of this.slotData) {
  94. const key = `${slot.row}`
  95. if (!grouped[key]) grouped[key] = []
  96. grouped[key].push(slot)
  97. }
  98. const rows = Object.keys(grouped).sort((a, b) => Number(a) - Number(b))
  99. const startY = 20
  100. const rowHeight = 120
  101. const rowGap = 20
  102. for (let i = 0; i < rows.length; i++) {
  103. const rowSlots = grouped[rows[i]]
  104. await this.renderSlotRow(rowSlots, startY + i * (rowHeight + rowGap), rowHeight)
  105. }
  106. this.layer.draw()
  107. },
  108. async renderSlotRow(slots, boxTopY, boxHeight) {
  109. const padding = 20
  110. const boxWidth = 860
  111. const centerX = this.stage.width() / 2
  112. const boxStartX = centerX - boxWidth / 2
  113. const box = new Konva.Rect({
  114. x: boxStartX,
  115. y: boxTopY,
  116. width: boxWidth,
  117. height: boxHeight,
  118. stroke: 'black',
  119. strokeWidth: 2
  120. })
  121. this.layer.add(box)
  122. const loadedImages = []
  123. for (const slot of slots) {
  124. const { slotType, isOccupied } = slot
  125. let baseKey = ''
  126. if (slotType === '0') {
  127. baseKey = isOccupied === '1' ? 'icon.locker.normal' : 'icon.locker.out'
  128. } else {
  129. baseKey = isOccupied === '1' ? 'icon.padlock.normal' : 'icon.padlock.out'
  130. }
  131. const baseUrl = this.cachedResults[baseKey]
  132. if (!baseUrl || !this.cachedImages[baseUrl]) continue
  133. const imageNode = this.cachedImages[baseUrl].clone()
  134. const width = slotType === '0' ? 110 : 40
  135. const height = 90
  136. imageNode.setAttrs({ width, height })
  137. loadedImages.push({ imageNode, slot, width, height })
  138. }
  139. const totalSlots = loadedImages.length
  140. const spacing = totalSlots > 1
  141. ? (boxWidth - 2 * padding - totalSlots * loadedImages[0].width) / (totalSlots - 1)
  142. : 0
  143. let currentX = boxStartX + padding
  144. for (const { imageNode, slot, width, height } of loadedImages) {
  145. imageNode.setAttrs({
  146. x: currentX,
  147. y: boxTopY + (boxHeight - height) / 2
  148. })
  149. this.layer.add(imageNode)
  150. if (slot.status === '1') {
  151. const exUrl = this.cachedResults['icon.locker.exception']
  152. if (exUrl && this.cachedImages[exUrl]) {
  153. const exImage = this.cachedImages[exUrl].clone()
  154. const exWidth = 30
  155. const exHeight = 30
  156. exImage.setAttrs({
  157. x: currentX + (width - exWidth) / 2,
  158. y: boxTopY + (boxHeight - exHeight) / 2,
  159. width: exWidth,
  160. height: exHeight
  161. })
  162. exImage.on('click', () => {
  163. this.showErrorDialog(slot)
  164. })
  165. this.layer.add(exImage)
  166. }
  167. }
  168. currentX += width + spacing
  169. }
  170. },
  171. loadImageOnce(url) {
  172. if (this.cachedImages[url]) return Promise.resolve(this.cachedImages[url])
  173. return new Promise((resolve, reject) => {
  174. const img = new window.Image()
  175. img.crossOrigin = 'Anonymous'
  176. img.src = url
  177. img.onload = () => {
  178. const konvaImage = new Konva.Image({ image: img })
  179. this.cachedImages[url] = konvaImage
  180. resolve(konvaImage)
  181. }
  182. img.onerror = reject
  183. })
  184. }
  185. }
  186. }
  187. </script>
  188. <style scoped lang="scss">
  189. .time-card {
  190. display: inline-flex;
  191. align-items: center;
  192. padding: 12px 20px;
  193. background: linear-gradient(135deg, #f5f7fa 0%, #e4e8eb 100%);
  194. border-radius: 8px;
  195. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  196. transition: all 0.3s;
  197. }
  198. .time-card.glow {
  199. box-shadow: 0 0 15px rgba(64, 158, 255, 0.5);
  200. }
  201. .time-card i {
  202. font-size: 24px;
  203. color: #409EFF;
  204. margin-right: 12px;
  205. }
  206. .time-content .label {
  207. font-size: 12px;
  208. color: #909399;
  209. }
  210. .time-content .time {
  211. font-size: 16px;
  212. font-weight: bold;
  213. color: #303133;
  214. }
  215. </style>