EnvMonitorStats.vue 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347
  1. <template>
  2. <div class="panel env-monitor-stats">
  3. <div class="border-beam"></div>
  4. <svg class="pc tl" viewBox="0 0 14 14"><path d="M0 14 V0 H14" fill="none" stroke="rgba(30,144,255,0.85)" stroke-width="1.5"/></svg>
  5. <svg class="pc tr" viewBox="0 0 14 14"><path d="M0 14 V0 H14" fill="none" stroke="rgba(30,144,255,0.85)" stroke-width="1.5"/></svg>
  6. <svg class="pc bl" viewBox="0 0 14 14"><path d="M0 14 V0 H14" fill="none" stroke="rgba(30,144,255,0.85)" stroke-width="1.5"/></svg>
  7. <svg class="pc br" viewBox="0 0 14 14"><path d="M0 14 V0 H14" fill="none" stroke="rgba(30,144,255,0.85)" stroke-width="1.5"/></svg>
  8. <div class="panel-header">
  9. <div class="panel-header-icon">📡</div>
  10. <span class="panel-title">智能环境感知应用设备统计</span>
  11. </div>
  12. <div class="panel-body">
  13. <!-- 在线/离线设备 -->
  14. <div class="device-online-row">
  15. <div class="device-stat-chip online">
  16. <div class="dv">{{ online }}</div>
  17. <div class="dl">在线设备</div>
  18. </div>
  19. <div class="device-stat-chip offline">
  20. <div class="dv">{{ offline }}</div>
  21. <div class="dl">离线设备</div>
  22. </div>
  23. </div>
  24. <!-- 仪表盘 + 设备列表 -->
  25. <div class="device-middle">
  26. <div ref="gauge" class="gauge-chart"></div>
  27. <div style="flex:1;display:flex;flex-direction:column;gap:12px">
  28. <div style="display:flex;gap:12px">
  29. <div class="device-list-item" style="flex:1" v-for="item in devicesRow1" :key="item.name">
  30. <div class="dli-icon">{{ item.icon }}</div>
  31. <div class="dli-num">{{ item.value }}</div>
  32. <div class="dli-name">{{ item.name }}</div>
  33. </div>
  34. </div>
  35. <div style="display:flex;gap:12px;justify-content:center">
  36. <div class="device-list-item" style="flex:0 0 calc(33.33% - 6px)" v-for="item in devicesRow2" :key="item.name">
  37. <div class="dli-icon">{{ item.icon }}</div>
  38. <div class="dli-num">{{ item.value }}</div>
  39. <div class="dli-name">{{ item.name }}</div>
  40. </div>
  41. </div>
  42. </div>
  43. </div>
  44. </div>
  45. </div>
  46. </template>
  47. <script>
  48. import * as echarts from 'echarts'
  49. import { getIotDeviceStatistics } from '@/api/screen'
  50. export default {
  51. name: 'EnvMonitorStats',
  52. data() {
  53. return {
  54. online: 0,
  55. offline: 0,
  56. onlineRate: 0,
  57. devices: [],
  58. chart: null,
  59. pollTimer: null
  60. }
  61. },
  62. computed: {
  63. devicesRow1() { return this.devices.slice(0, 3) },
  64. devicesRow2() { return this.devices.slice(3) }
  65. },
  66. mounted() {
  67. this.fetchData()
  68. this.pollTimer = setInterval(this.fetchData, 5 * 60 * 1000)
  69. },
  70. beforeDestroy() {
  71. if (this.pollTimer) clearInterval(this.pollTimer)
  72. if (this.chart) this.chart.dispose()
  73. window.removeEventListener('resize', this.handleResize)
  74. },
  75. methods: {
  76. async fetchData() {
  77. try {
  78. const res = await getIotDeviceStatistics()
  79. if (res.code === 200) {
  80. const d = res.data
  81. this.online = d.onlineCount
  82. this.offline = d.offlineCount
  83. this.onlineRate = d.onlineRate
  84. this.devices = [
  85. { icon: '🏷️', value: d.tagCount, name: '电子信息铭牌' },
  86. { icon: '⚗️', value: d.terminalCount, name: '化学品智能终端' },
  87. { icon: '🌡️', value: d.sensorCount, name: '传感器' },
  88. { icon: '📷', value: d.cameraCount, name: '智慧摄像头' },
  89. { icon: '📡', value: d.iotCount, name: '其他智能设备' }
  90. ]
  91. }
  92. } catch (e) {
  93. // 保持默认值
  94. }
  95. if (this.chart) {
  96. this.chart.dispose()
  97. this.chart = null
  98. }
  99. this.$nextTick(() => this.initChart())
  100. },
  101. handleResize() {
  102. if (this.chart) this.chart.resize()
  103. },
  104. /** 初始化仪表盘 - 展示设备在线率 */
  105. initChart() {
  106. this.chart = echarts.init(this.$refs.gauge, null, { renderer: 'canvas', devicePixelRatio: 2 })
  107. const option = {
  108. backgroundColor: 'transparent',
  109. series: [{
  110. type: 'gauge',
  111. radius: '90%',
  112. center: ['50%', '60%'],
  113. startAngle: 210,
  114. endAngle: -30,
  115. min: 0,
  116. max: 100,
  117. splitNumber: 5,
  118. axisLine: {
  119. lineStyle: {
  120. width: 30,
  121. color: [
  122. [0.3, '#ef4444'],
  123. [0.6, '#f59e0b'],
  124. [1, '#1e90ff']
  125. ]
  126. }
  127. },
  128. axisTick: { show: false },
  129. splitLine: { show: false },
  130. axisLabel: { show: false },
  131. pointer: {
  132. icon: 'path://M12.8,0.7l12.3,0 M0,0 l9.2,12.4 M0,0 l2.2,-1.6',
  133. offsetCenter: [0, '-60%'],
  134. width: 8,
  135. length: '60%',
  136. itemStyle: { color: '#1e90ff' }
  137. },
  138. detail: {
  139. valueAnimation: true,
  140. formatter: '{value}%',
  141. color: '#ffd740',
  142. fontSize: 45,
  143. fontWeight: 700,
  144. offsetCenter: [0, '30%']
  145. },
  146. title: {
  147. show: true,
  148. offsetCenter: [0, '62%'],
  149. color: 'rgba(110,165,210,0.6)',
  150. fontSize: 25
  151. },
  152. data: [{ value: this.onlineRate, name: '在线率' }]
  153. }]
  154. }
  155. this.chart.setOption(option)
  156. window.addEventListener('resize', this.handleResize)
  157. }
  158. }
  159. }
  160. </script>
  161. <style lang="scss" scoped>
  162. .env-monitor-stats {
  163. position: relative;
  164. display: flex;
  165. flex-direction: column;
  166. }
  167. /* ---- Border beam ---- */
  168. .border-beam {
  169. position: absolute;
  170. inset: 0;
  171. pointer-events: none;
  172. border-radius: inherit;
  173. overflow: hidden;
  174. z-index: 2;
  175. &::before {
  176. content: '';
  177. position: absolute;
  178. top: 0;
  179. left: -100%;
  180. width: 40%;
  181. height: 3px;
  182. background: linear-gradient(90deg, transparent, rgba(30, 144, 255, 0.9), rgba(0, 216, 255, 0.7), transparent);
  183. animation: beamTop 5s linear infinite;
  184. }
  185. &::after {
  186. content: '';
  187. position: absolute;
  188. bottom: 0;
  189. right: -100%;
  190. width: 40%;
  191. height: 3px;
  192. background: linear-gradient(90deg, transparent, rgba(0, 216, 255, 0.7), rgba(30, 144, 255, 0.9), transparent);
  193. animation: beamBottom 5s linear infinite 2.5s;
  194. }
  195. }
  196. @keyframes beamTop { from { left: -40%; } to { left: 100%; } }
  197. @keyframes beamBottom { from { right: -40%; } to { right: 100%; } }
  198. /* ---- Corner ornaments ---- */
  199. .pc {
  200. position: absolute;
  201. width: 35px;
  202. height: 35px;
  203. z-index: 3;
  204. pointer-events: none;
  205. &.tl { top: 0; left: 0; }
  206. &.tr { top: 0; right: 0; transform: scaleX(-1); }
  207. &.bl { bottom: 0; left: 0; transform: scaleY(-1); }
  208. &.br { bottom: 0; right: 0; transform: scale(-1); }
  209. svg { width: 100%; height: 100%; }
  210. }
  211. /* ---- Panel header ---- */
  212. .panel-header {
  213. display: flex;
  214. align-items: center;
  215. gap: 25px;
  216. padding: 20px 30px 18px;
  217. border-bottom: 1px solid $border;
  218. background: linear-gradient(90deg, rgba(0, 60, 160, 0.18), transparent);
  219. flex-shrink: 0;
  220. }
  221. .panel-header-icon {
  222. width: 65px;
  223. height: 65px;
  224. border-radius: 12px;
  225. flex-shrink: 0;
  226. display: flex;
  227. align-items: center;
  228. justify-content: center;
  229. font-size: 32px;
  230. background: linear-gradient(135deg, rgba(30, 144, 255, 0.25), rgba(0, 216, 255, 0.15));
  231. border: 1px solid rgba(30, 144, 255, 0.35);
  232. animation: iconGlow 3s ease-in-out infinite;
  233. }
  234. @keyframes iconGlow {
  235. 0%, 100% { box-shadow: 0 0 15px rgba(30, 144, 255, 0.3); }
  236. 50% { box-shadow: 0 0 35px rgba(30, 144, 255, 0.7), 0 0 12px rgba(0, 216, 255, 0.3); }
  237. }
  238. .panel-title {
  239. font-size: 30px;
  240. font-weight: 600;
  241. letter-spacing: 2px;
  242. color: $cyan;
  243. }
  244. /* ---- Panel body ---- */
  245. .panel-body {
  246. padding: 20px 25px;
  247. }
  248. /* ---- Online / Offline row ---- */
  249. .device-online-row {
  250. display: flex;
  251. gap: 18px;
  252. margin-bottom: 18px;
  253. }
  254. .device-stat-chip {
  255. flex: 1;
  256. padding: 15px;
  257. border-radius: 10px;
  258. text-align: center;
  259. background: $bg-card;
  260. border: 1px solid $border;
  261. &.online {
  262. border-color: $green;
  263. }
  264. &.offline {
  265. border-color: $red;
  266. }
  267. .dv {
  268. font-size: 50px;
  269. font-weight: 700;
  270. }
  271. &.online .dv {
  272. color: $green;
  273. }
  274. &.offline .dv {
  275. color: $red;
  276. }
  277. .dl {
  278. font-size: 25px;
  279. color: $text-dim;
  280. }
  281. }
  282. /* ---- Middle: gauge + device grid ---- */
  283. .device-middle {
  284. display: flex;
  285. gap: 20px;
  286. align-items: center;
  287. }
  288. .gauge-chart {
  289. flex: 0 0 295px;
  290. height: 295px;
  291. }
  292. .device-list-item {
  293. padding: 15px;
  294. border-radius: 10px;
  295. background: $bg-card;
  296. border: 1px solid $border;
  297. display: flex;
  298. flex-direction: column;
  299. align-items: center;
  300. gap: 8px;
  301. .dli-icon {
  302. font-size: 40px;
  303. }
  304. .dli-num {
  305. font-size: 38px;
  306. font-weight: 700;
  307. color: $gold;
  308. }
  309. .dli-name {
  310. font-size: 22px;
  311. color: $text-dim;
  312. text-align: center;
  313. line-height: 1.3;
  314. }
  315. }
  316. </style>