LineChart.vue 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317
  1. <script setup lang="ts">
  2. import { computed, onMounted, onUnmounted, ref } from 'vue';
  3. import VChart from 'vue-echarts';
  4. import { LineChart, PieChart } from 'echarts/charts';
  5. import {
  6. DatasetComponent,
  7. GridComponent,
  8. LegendComponent,
  9. MarkLineComponent,
  10. TitleComponent,
  11. TooltipComponent,
  12. TransformComponent,
  13. } from 'echarts/components';
  14. import { use } from 'echarts/core';
  15. import { CanvasRenderer } from 'echarts/renderers';
  16. import SvgIcon from './SvgIcon.vue';
  17. import type { MonitoringPointData } from '@/types';
  18. use([
  19. CanvasRenderer,
  20. TitleComponent,
  21. DatasetComponent,
  22. TransformComponent,
  23. TooltipComponent,
  24. LegendComponent,
  25. GridComponent,
  26. LineChart,
  27. PieChart,
  28. MarkLineComponent,
  29. ]);
  30. const list = ref<number[]>([]);
  31. interface Props {
  32. data: MonitoringPointData;
  33. monitoringId?: number;
  34. iconShow: boolean;
  35. }
  36. const emit = defineEmits(['editorClick', 'historicalDataClick']);
  37. const props = defineProps<Props>();
  38. const getCurvedData = () => {
  39. list.value = [];
  40. if (props.data.tempData) {
  41. props.data.tempData.forEach((item) => {
  42. list.value.push(item.value);
  43. });
  44. }
  45. };
  46. const getColor = (value: number) => {
  47. if (props.data.status === 1) {
  48. if (value === 1) {
  49. return '#32BAC0';
  50. } else if (value === 2) {
  51. return 'rgba(50,186,192,0.15)';
  52. }
  53. }
  54. if (props.data.status === 2 || props.data.status === 3) {
  55. if (value === 1) {
  56. return '#F56C6C';
  57. } else if (value === 2) {
  58. return 'rgba(245,108,108,0.15)';
  59. }
  60. }
  61. if (props.data.status === 0 || props.data.status === -1 || !props.data.status) {
  62. if (value === 1) {
  63. return '#999';
  64. } else if (value === 2) {
  65. return 'rgba(153,153,153,0.15)';
  66. }
  67. }
  68. };
  69. const option = computed(() => {
  70. getCurvedData();
  71. if (!props.data) return {};
  72. return {
  73. backgroundColor: getColor(2),
  74. xAxis: {
  75. show: false, // 隐藏 X 轴
  76. type: 'category',
  77. boundaryGap: false, // 关闭首尾留白
  78. },
  79. yAxis: {
  80. type: 'value', // 数值轴
  81. min: 0, // 最小值
  82. max: 100, // 最大值
  83. interval: 20, // 刻度间隔
  84. show: false, // 隐藏 X 轴
  85. },
  86. series: [
  87. {
  88. type: 'line',
  89. data: list.value,
  90. symbol: 'none', // 不显示数据点
  91. color: getColor(1),
  92. lineStyle: {
  93. width: 2,
  94. },
  95. markLine: {
  96. // 添加最大值虚线
  97. silent: true,
  98. symbol: 'none',
  99. lineStyle: {
  100. type: 'dashed',
  101. color: 'rgba(151,151,151,0.5)',
  102. width: 1,
  103. },
  104. label: {
  105. show: true, // 必须开启标签
  106. position: 'insideStartTop', // 标签位置(左侧上方)
  107. formatter: '{c}°C', // 显示值({c} 会自动替换为 yAxis 的值)
  108. color: 'rgba(0,0,0,0.5)', // 文字颜色
  109. verticalAlign: 'bottom', // 垂直对齐方式
  110. offset: [-6, 0], // 微调位置 [水平偏移, 垂直偏移]
  111. fontSize: '10px',
  112. },
  113. data: [
  114. {
  115. yAxis: props.data.tempUpper, // 在最大值位置画线
  116. },
  117. ],
  118. },
  119. },
  120. ],
  121. // 关键配置:调整 grid 边距
  122. grid: {
  123. top: 20, // 上边距为0
  124. right: 8, // 右边距为0
  125. bottom: 8, // 下边距为0
  126. left: 8, // 左边距为0
  127. containLabel: false, // 不包含坐标轴标签区域
  128. },
  129. };
  130. });
  131. const textContainer = ref<HTMLElement | null>(null);
  132. const shouldShowEllipsis = ref(false);
  133. // 判断是否溢出
  134. const checkOverflow = () => {
  135. if (!textContainer.value) return;
  136. const element = textContainer.value;
  137. shouldShowEllipsis.value = element.scrollWidth > element.clientWidth;
  138. };
  139. // 自动响应式处理
  140. const handleResize = () => {
  141. checkOverflow();
  142. };
  143. const editorMonitoring = (id: number) => {
  144. emit('editorClick', id);
  145. };
  146. const addHistoricalData = (data: MonitoringPointData) => {
  147. emit('historicalDataClick', data);
  148. };
  149. // 生命周期钩子
  150. onMounted(() => {
  151. checkOverflow();
  152. window.addEventListener('resize', handleResize);
  153. });
  154. onUnmounted(() => {
  155. window.removeEventListener('resize', handleResize);
  156. });
  157. </script>
  158. <template>
  159. <div :class="monitoringId === data.id ? 'line-chart-content choose-content-border' : 'line-chart-content'">
  160. <AFlex justify="space-between" align="center">
  161. <APopover v-if="shouldShowEllipsis">
  162. <template #content>
  163. {{ data.name }}
  164. </template>
  165. <div ref="textContainer" class="line-chart-header-text">{{ data.name }}</div>
  166. </APopover>
  167. <div v-else ref="textContainer" class="line-chart-header-text">{{ data.name }}</div>
  168. <SvgIcon v-if="iconShow" class="line-chart-header-icon" @click="editorMonitoring(data.id)" name="adjustment" />
  169. </AFlex>
  170. <div @click="addHistoricalData(data)" class="chart-container" style="cursor: pointer">
  171. <VChart class="chart chart-bgc" :option="option" />
  172. </div>
  173. <AFlex justify="space-between" align="center" class="line-chart-footer">
  174. <AFlex align="center">
  175. <AFlex
  176. justify="center"
  177. align="center"
  178. class="line-chart-footer-div"
  179. :style="`background:${props.data.status === 0 || props.data.status === -1 || !props.data.status ? '#F0F0F0' : '#f2fcf9'}`"
  180. >
  181. <SvgIcon
  182. class="line-chart-icon"
  183. :style="`color:${props.data.status === 0 || props.data.status === -1 || !props.data.status ? '#999' : '#32BAC0'}`"
  184. name="temperature"
  185. /></AFlex>
  186. <div>
  187. <span class="line-chart-footer-text">{{ data.temperature }}</span>
  188. <span class="line-chart-footer-unit">℃</span>
  189. </div>
  190. </AFlex>
  191. <AFlex align="center">
  192. <AFlex
  193. justify="center"
  194. align="center"
  195. class="line-chart-footer-div"
  196. :style="`background:${props.data.status === 0 || props.data.status === -1 || !props.data.status ? '#F0F0F0' : '#f2fcf9'}`"
  197. ><SvgIcon
  198. class="line-chart-icon"
  199. :style="`color:${props.data.status === 0 || props.data.status === -1 || !props.data.status ? '#999' : '#32BAC0'}`"
  200. name="humidity"
  201. /></AFlex>
  202. <div>
  203. <span class="line-chart-footer-text">{{ data.humidity }}</span>
  204. <span class="line-chart-footer-unit">%</span>
  205. </div>
  206. </AFlex>
  207. </AFlex>
  208. </div>
  209. </template>
  210. <style lang="scss" scoped>
  211. /* 确保图表区域继承光标样式 */
  212. .chart-container :deep(canvas) {
  213. cursor: pointer !important;
  214. }
  215. .chart-bgc {
  216. width: 172px;
  217. height: 94px;
  218. margin-top: 12px; /* 关键:隐藏图表溢出部分 */
  219. overflow: hidden;
  220. cursor: pointer;
  221. border-radius: 8px; /* 圆角 */
  222. }
  223. .line-chart-icon {
  224. font-size: 16px;
  225. color: #32bac0;
  226. }
  227. .line-chart-footer-unit {
  228. font-size: 12px;
  229. font-style: normal;
  230. font-weight: 400;
  231. line-height: 20px;
  232. color: rgb(0 0 0 / 85%);
  233. text-align: left;
  234. }
  235. .line-chart-footer-text {
  236. font-size: 16px;
  237. font-style: normal;
  238. font-weight: 500;
  239. line-height: 28px;
  240. color: rgb(0 0 0 / 85%);
  241. text-align: left;
  242. }
  243. .line-chart-footer-div {
  244. width: 30px;
  245. height: 30px;
  246. background: #f2fcf9;
  247. border-radius: 8px;
  248. }
  249. .line-chart-footer {
  250. margin-top: 12px;
  251. }
  252. .line-chart-header-icon {
  253. cursor: pointer;
  254. }
  255. .line-chart-header-text {
  256. max-width: 160px;
  257. overflow: hidden; /* 隐藏溢出内容 */
  258. font-size: 16px;
  259. font-style: normal;
  260. font-weight: 400;
  261. line-height: 24px;
  262. color: rgb(0 0 0 / 85%);
  263. text-align: left;
  264. text-overflow: ellipsis; /* 显示省略号 */
  265. white-space: nowrap; /* 强制不换行 */
  266. }
  267. .line-chart-content {
  268. width: 204px;
  269. height: 204px;
  270. padding: 16px;
  271. margin: 2px 4px 14px;
  272. background: #fff;
  273. border-radius: 8px;
  274. box-shadow: 0 2px 6px 0 rgb(0 0 0 / 10%);
  275. }
  276. .choose-content-border {
  277. border: 2px solid var(--antd-color-primary-hover);
  278. }
  279. .line-chart-content:hover {
  280. border: 2px solid var(--antd-color-primary-hover);
  281. }
  282. </style>