rd-device-category.vue 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307
  1. <script lang="ts" setup>
  2. import * as echarts from 'echarts'
  3. import { IotStatApi } from '@/api/pms/stat'
  4. import { ANIMATION, CHART_RENDERER, createTooltip, FONT_FAMILY, THEME } from '@/utils/kb'
  5. interface DeviceCategoryItem {
  6. name: string
  7. count: number
  8. type: string
  9. code?: string | number
  10. }
  11. interface DeviceCategoryResponseItem {
  12. className?: string
  13. count?: number | string
  14. type?: string
  15. code?: string | number
  16. }
  17. const { push } = useRouter()
  18. const chartRef = ref<HTMLDivElement>()
  19. const loading = ref(false)
  20. const categoryData = ref<DeviceCategoryItem[]>([])
  21. let chart: echarts.ECharts | null = null
  22. const TOP_BAR_COLOR = new echarts.graphic.LinearGradient(0, 0, 1, 0, [
  23. { offset: 0, color: '#ffd166' },
  24. { offset: 0.58, color: '#ff9f43' },
  25. { offset: 1, color: '#f76707' }
  26. ])
  27. const DEFAULT_BAR_COLOR = new echarts.graphic.LinearGradient(0, 0, 1, 0, [
  28. { offset: 0, color: THEME.color.blue.light },
  29. { offset: 0.58, color: THEME.color.blue.mid },
  30. { offset: 1, color: THEME.color.blue.line }
  31. ])
  32. function sortCategoryData(data: DeviceCategoryItem[]) {
  33. const typeOrder = (type: string) => {
  34. if (type === 'top') return 0
  35. if (type === '') return 1
  36. return 2
  37. }
  38. return data.sort((a, b) => typeOrder(a.type) - typeOrder(b.type))
  39. }
  40. function getChartLayout() {
  41. const { clientWidth = 0, clientHeight = 0 } = chartRef.value ?? {}
  42. const compact = clientHeight > 0 && (clientHeight < 210 || clientWidth < 520)
  43. return {
  44. gridTop: compact ? 20 : 26,
  45. gridRight: compact ? 32 : 40,
  46. gridBottom: compact ? 22 : 28,
  47. gridLeft: compact ? 12 : 12,
  48. barWidth: compact ? 10 : 14,
  49. axisFontSize: compact ? 10 : 12,
  50. labelFontSize: compact ? 10 : 12
  51. }
  52. }
  53. function formatValue(value: number) {
  54. return Number(value || 0).toLocaleString('en-US')
  55. }
  56. function getChartOption(): echarts.EChartsOption {
  57. const layout = getChartLayout()
  58. const values = categoryData.value.map((item) => item.count)
  59. const maxValue = Math.max(...values, 1)
  60. const xAxisMax = Math.ceil((maxValue * 1.12) / 10) * 10
  61. return {
  62. ...ANIMATION,
  63. grid: {
  64. ...THEME.grid,
  65. top: layout.gridTop,
  66. right: layout.gridRight,
  67. bottom: layout.gridBottom,
  68. left: layout.gridLeft
  69. },
  70. tooltip: createTooltip({
  71. trigger: 'axis',
  72. axisPointer: {
  73. type: 'shadow',
  74. shadowStyle: {
  75. color: THEME.split
  76. }
  77. },
  78. formatter(params: any[]) {
  79. const first = params[0]
  80. const row = categoryData.value[first?.dataIndex ?? 0]
  81. if (!row) return ''
  82. return [
  83. `<div style="min-width:132px;font-weight:700;color:#24364f;margin-bottom:6px;">${row.name}</div>`,
  84. `<div style="display:flex;justify-content:space-between;gap:18px;line-height:22px;"><span>设备数量</span><b style="color:#1f5bb8;">${row.count} 台</b></div>`
  85. ].join('')
  86. }
  87. }),
  88. xAxis: {
  89. type: 'value',
  90. name: '台',
  91. max: xAxisMax,
  92. axisLine: {
  93. show: false
  94. },
  95. axisTick: {
  96. show: false
  97. },
  98. axisLabel: {
  99. color: THEME.text.regular,
  100. fontSize: layout.axisFontSize,
  101. fontFamily: FONT_FAMILY,
  102. formatter(value: number) {
  103. return formatValue(value)
  104. }
  105. },
  106. nameTextStyle: {
  107. color: '#4c6c9b',
  108. fontSize: layout.axisFontSize,
  109. fontWeight: 600,
  110. fontFamily: FONT_FAMILY
  111. },
  112. splitLine: {
  113. lineStyle: {
  114. color: 'rgba(31, 91, 184, 0.24)',
  115. type: 'dashed'
  116. }
  117. }
  118. },
  119. yAxis: {
  120. type: 'category',
  121. data: categoryData.value.map((item) => item.name),
  122. inverse: true,
  123. axisLine: {
  124. show: false
  125. },
  126. axisTick: {
  127. show: false
  128. },
  129. axisLabel: {
  130. color: '#102a43',
  131. fontSize: layout.axisFontSize,
  132. fontWeight: 600,
  133. fontFamily: FONT_FAMILY,
  134. width: 80,
  135. overflow: 'truncate'
  136. }
  137. },
  138. series: [
  139. {
  140. name: '设备数量',
  141. type: 'bar',
  142. data: categoryData.value.map((item) => ({
  143. value: item.count,
  144. code: item.code,
  145. itemStyle: {
  146. color: item.type === 'top' ? TOP_BAR_COLOR : DEFAULT_BAR_COLOR
  147. }
  148. })),
  149. barWidth: layout.barWidth,
  150. cursor: 'pointer',
  151. showBackground: true,
  152. backgroundStyle: {
  153. color: 'rgba(31, 91, 184, 0.08)',
  154. borderRadius: 999
  155. },
  156. itemStyle: {
  157. borderRadius: 999
  158. },
  159. label: {
  160. show: true,
  161. position: 'right',
  162. distance: 8,
  163. color: THEME.text.strong,
  164. fontSize: layout.labelFontSize,
  165. fontWeight: 700,
  166. fontFamily: FONT_FAMILY,
  167. formatter(params: any) {
  168. return `${formatValue(params.value)}`
  169. }
  170. },
  171. emphasis: {
  172. itemStyle: {
  173. shadowBlur: 2
  174. // shadowColor: THEME.color.blue.shadow
  175. }
  176. }
  177. }
  178. ]
  179. }
  180. }
  181. function initChart() {
  182. if (!chartRef.value) return
  183. chart?.dispose()
  184. chart = echarts.init(chartRef.value, undefined, {
  185. renderer: CHART_RENDERER
  186. })
  187. bindChartEvents()
  188. renderChart()
  189. }
  190. function bindChartEvents() {
  191. chart?.off('click')
  192. chart?.on('click', (params) => {
  193. const row = categoryData.value[params.dataIndex ?? -1]
  194. if (!row?.code) return
  195. push({
  196. name: 'IotDevicePms',
  197. query: {
  198. code: String(row.code)
  199. }
  200. })
  201. })
  202. }
  203. function renderChart() {
  204. chart?.setOption(getChartOption(), true)
  205. }
  206. function resizeChart() {
  207. chart?.resize()
  208. renderChart()
  209. }
  210. function destroyChart() {
  211. chart?.dispose()
  212. chart = null
  213. }
  214. async function getDeviceCategory() {
  215. loading.value = true
  216. try {
  217. const res = await IotStatApi.getRdNewClassify()
  218. categoryData.value = Array.isArray(res)
  219. ? sortCategoryData(
  220. res.map((item: DeviceCategoryResponseItem) => ({
  221. name: item.className || '未知',
  222. count: Number(item.count ?? 0),
  223. type: item.type || '',
  224. code: item.code
  225. }))
  226. ).slice(0, 10)
  227. : []
  228. renderChart()
  229. } catch (error) {
  230. console.error('获取瑞都设备分类失败:', error)
  231. categoryData.value = []
  232. renderChart()
  233. } finally {
  234. loading.value = false
  235. }
  236. }
  237. onMounted(() => {
  238. initChart()
  239. getDeviceCategory()
  240. window.addEventListener('resize', resizeChart)
  241. window.addEventListener('rdkb:resize', resizeChart)
  242. })
  243. onUnmounted(() => {
  244. window.removeEventListener('resize', resizeChart)
  245. window.removeEventListener('rdkb:resize', resizeChart)
  246. destroyChart()
  247. })
  248. </script>
  249. <template>
  250. <div class="panel flex flex-col">
  251. <div class="panel-title">
  252. <div class="icon-decorator">
  253. <span></span>
  254. <span></span>
  255. </div>
  256. 设备分类
  257. </div>
  258. <div
  259. v-loading="loading"
  260. element-loading-text="加载中..."
  261. element-loading-background="rgb(222 236 252 / 72%)"
  262. class="device-category-body">
  263. <div ref="chartRef" class="device-category-chart"></div>
  264. </div>
  265. </div>
  266. </template>
  267. <style lang="scss" scoped>
  268. @import url('@/styles/kb.scss');
  269. .device-category-body {
  270. display: flex;
  271. width: 100%;
  272. min-height: 0;
  273. flex: 1;
  274. }
  275. .device-category-chart {
  276. width: 100%;
  277. min-height: 0;
  278. flex: 1;
  279. }
  280. </style>