TempStressCard.vue 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228
  1. <template>
  2. <!-- 设备温度应力趋势卡片,通过父组件 visible 控制显隐 -->
  3. <div class="middle-area-content" v-if="visible">
  4. <div class="data-card">
  5. <div class="card-header">
  6. <!-- 标题:选中设备时显示"设备名 数据",否则显示默认标题 -->
  7. <h3 class="card-title">{{ device ? device.name + ' 数据' : '温度与应力监测' }}</h3>
  8. <div class="header-actions">
  9. <!-- 选中设备时显示设备路径面包屑 -->
  10. <span class="update-time" v-if="device">{{ breadcrumb }}</span>
  11. <span class="update-time" v-else>单位:温度(℃) / 应力(MPa)</span>
  12. <!-- 日期范围选择器:开始 / 结束 -->
  13. <span class="panel-date-label">开始</span>
  14. <input class="panel-date-input" type="date" v-model="startDate" @change="renderChart" />
  15. <span class="panel-date-label">结束</span>
  16. <input class="panel-date-input" type="date" v-model="endDate" @change="renderChart" />
  17. <!-- 关闭按钮(X),触发父组件 close 事件 -->
  18. <svg class="panel-close-btn" @click="$emit('close')" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="#7bbef6" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
  19. </div>
  20. </div>
  21. <div class="card-body chart-body">
  22. <div class="monitor-chart-box">
  23. <div class="monitor-chart-label">温度 / 应力趋势</div>
  24. <!-- ECharts 渲染容器:去掉固定宽高,用 100% 占满父容器 -->
  25. <div class="temp-chart-wrapper"><div id="tempStressChart" style="width:100%;height:200px;"></div></div>
  26. </div>
  27. <!-- 摘要数据卡片:当前温度、最大应力、平均温度、平均应力 -->
  28. <div class="water-stats-row" style="margin-top:4px;">
  29. <div class="water-stat-card">
  30. <div class="water-stat-label">当前温度</div>
  31. <div class="water-stat-value">28.6</div>
  32. </div>
  33. <div class="water-stat-card">
  34. <div class="water-stat-label">最大应力</div>
  35. <div class="water-stat-value">2.45</div>
  36. </div>
  37. <div class="water-stat-card">
  38. <div class="water-stat-label">平均温度</div>
  39. <div class="water-stat-value">23.8</div>
  40. </div>
  41. <div class="water-stat-card">
  42. <div class="water-stat-label">平均应力</div>
  43. <div class="water-stat-value">1.82</div>
  44. </div>
  45. </div>
  46. </div>
  47. </div>
  48. </div>
  49. </template>
  50. <script>
  51. import * as echarts from "echarts";
  52. export default {
  53. name: "TempStressCard",
  54. props: {
  55. device: { type: Object, default: null },
  56. breadcrumb: { type: String, default: "" },
  57. visible: { type: Boolean, default: false },
  58. },
  59. data() {
  60. return {
  61. startDate: "2026-01-01",
  62. endDate: "2026-12-31",
  63. chart: null,
  64. };
  65. },
  66. watch: {
  67. visible(val) {
  68. if (val) {
  69. this.startDate = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString().slice(0, 10);
  70. this.endDate = new Date().toISOString().slice(0, 10);
  71. this.$nextTick(() => this.renderChart());
  72. } else {
  73. if (this.chart) {
  74. this.chart.dispose();
  75. this.chart = null;
  76. }
  77. }
  78. },
  79. },
  80. mounted() {
  81. window.addEventListener("resize", this.handleResize);
  82. },
  83. beforeUnmount() {
  84. window.removeEventListener("resize", this.handleResize);
  85. if (this.chart) this.chart.dispose();
  86. },
  87. methods: {
  88. handleResize() {
  89. if (!this.chart) return;
  90. this.chart.resize();
  91. },
  92. renderChart() {
  93. if (!echarts || !this.visible) return;
  94. if (this.chart) this.chart.dispose();
  95. const el = document.getElementById("tempStressChart");
  96. if (!el) return;
  97. const wrapper = el.parentElement;
  98. const dpr = window.devicePixelRatio || 1;
  99. // 让图表容器 100% 占满父容器
  100. el.style.width = "100%";
  101. el.style.height = "200px";
  102. this.chart = echarts.init(el, null, {
  103. renderer: "canvas",
  104. devicePixelRatio: dpr
  105. });
  106. const start = new Date(this.startDate);
  107. const end = new Date(this.endDate);
  108. const hours = Math.max(1, Math.round((end - start) / (60 * 60 * 1000)) + 1);
  109. const timeList = [];
  110. const tempData = [];
  111. const stressData = [];
  112. const seed = this.device ? this.device.name.charCodeAt(0) + this.device.name.length : 42;
  113. for (let i = 0; i < hours; i++) {
  114. const d = new Date(start.getTime() + i * 60 * 60 * 1000);
  115. const pad = n => String(n).padStart(2, "0");
  116. timeList.push((d.getMonth() + 1) + "/" + pad(d.getDate()) + " " + pad(d.getHours()) + ":00");
  117. const hourAngle = (d.getHours() / 24) * 2 * Math.PI;
  118. const dayOffset = i * 0.15;
  119. tempData.push(+(22 + 5 * Math.sin(hourAngle + dayOffset + seed) + 3 * Math.cos(dayOffset * 0.3) + (Math.random() - 0.5) * 1.5).toFixed(1));
  120. stressData.push(+(1.8 + 0.4 * Math.cos(hourAngle * 0.5 + dayOffset * 0.2 + seed) + 0.3 * Math.sin(dayOffset * 0.5) + (Math.random() - 0.5) * 0.2).toFixed(3));
  121. }
  122. const labelInterval = Math.max(1, Math.floor(hours / 12));
  123. this.chart.setOption({
  124. animation: false,
  125. tooltip: { trigger: "axis" },
  126. legend: {
  127. data: ["温度(℃)", "应力(MPa)"],
  128. textStyle: { color: "#7bbef6", fontSize: 10 },
  129. bottom: 2, // 图例紧贴底部,不占额外空间
  130. itemWidth: 10,
  131. itemHeight: 8,
  132. },
  133. // 👇 关键修改:扩大 grid 边距,让图表填满容器
  134. grid: {
  135. left: 40, // 给左Y轴留出空间
  136. right: 40, // 给右Y轴留出空间
  137. top: 15, // 顶部留少量空间
  138. bottom: 30, // 给X轴和图例留出空间
  139. containLabel: false // 取消自动包含标签,避免额外留白
  140. },
  141. xAxis: {
  142. type: "category",
  143. boundaryGap: false,
  144. data: timeList,
  145. axisLine: { lineStyle: { color: "#7bbef6" } },
  146. axisLabel: {
  147. color: "#7bbef6",
  148. fontSize: 8,
  149. interval: labelInterval,
  150. rotate: 0 // 保持水平不倾斜
  151. },
  152. },
  153. yAxis: [
  154. {
  155. type: "value",
  156. name: "温度(℃)",
  157. nameTextStyle: { color: "#62f6fb", fontSize: 10 },
  158. axisLine: { lineStyle: { color: "#62f6fb" } },
  159. axisLabel: { color: "#62f6fb", fontSize: 9 },
  160. splitLine: { lineStyle: { color: "rgba(123,190,246,0.15)" } },
  161. },
  162. {
  163. type: "value",
  164. name: "应力(MPa)",
  165. nameTextStyle: { color: "#f97316", fontSize: 10 },
  166. axisLine: { lineStyle: { color: "#f97316" } },
  167. axisLabel: { color: "#f97316", fontSize: 9 },
  168. splitLine: { show: false },
  169. },
  170. ],
  171. dataZoom: [
  172. { type: "inside", start: 0, end: 100 },
  173. ],
  174. series: [
  175. {
  176. name: "温度(℃)",
  177. type: "line",
  178. data: tempData,
  179. smooth: true,
  180. symbol: "none",
  181. lineStyle: { color: "#62f6fb", width: 1.5 },
  182. itemStyle: { color: "#62f6fb" },
  183. areaStyle: { color: "rgba(98,246,251,0.1)" },
  184. },
  185. {
  186. name: "应力(MPa)",
  187. type: "line",
  188. yAxisIndex: 1,
  189. data: stressData,
  190. smooth: true,
  191. symbol: "none",
  192. lineStyle: { color: "#f97316", width: 1.5 },
  193. itemStyle: { color: "#f97316" },
  194. areaStyle: { color: "rgba(249,115,22,0.08)" },
  195. },
  196. ],
  197. });
  198. },
  199. },
  200. };
  201. </script>
  202. <style scoped>
  203. .data-card { width: 100%; background: rgba(0,20,40,0.7); border-radius: 4px; overflow: hidden; box-shadow: 0 0 10px rgba(0,212,255,0.2); }
  204. .card-header { height: 42px; background-image: url("/src/assets/images/数据小标题.png"); background-size: 100% 100%; background-position: center; background-repeat: no-repeat; display: flex; align-items: flex-start; justify-content: space-between; padding: 4px 16px 0; }
  205. .card-title { font-size: var(--fs-card-title); font-weight: bold; color: #e0fcff; margin: 0; text-shadow: 0 0 5px rgba(0,212,255,0.5); padding-left: 20px; }
  206. .header-actions { display: flex; align-items: flex-start; gap: 10px; }
  207. .update-time { color: #7bbef6; font-size: 11px; font-family: monospace; }
  208. .card-body { padding: 8px; min-height: auto; font-size: 13px; line-height: 1.5; margin-top: -6px; }
  209. .card-body.chart-body { padding: 2px 8px 4px; }
  210. .monitor-chart-box { background: rgba(0,20,40,0.4); border-radius: 4px; border: 1px solid rgba(0,212,255,0.15); padding: 6px; }
  211. .monitor-chart-label { color: #62f6fb; font-size: var(--fs-label); font-weight: bold; margin-bottom: 2px; }
  212. .temp-chart-wrapper { width: 100%; }
  213. #tempStressChart { display: block; margin: 0 auto; }
  214. .water-stats-row { display: flex; gap: 6px; }
  215. .water-stat-card { flex: 1; display: flex; flex-direction: row; align-items: center; justify-content: space-between; padding: 3px 10px; background-image: url("/src/assets/images/卡片背景2.png"); background-size: 100% 100%; background-position: center; background-repeat: no-repeat; height: 42px; }
  216. .water-stat-label { color: #62f6fb; font-size: var(--fs-label); margin-bottom: 0; text-align: left; line-height: 1.2; white-space: nowrap; }
  217. .water-stat-value { color: #ffffff; font-size: var(--fs-value-xl); font-weight: bold; text-shadow: 0 0 6px rgba(0,212,255,0.5); line-height: 1.2; }
  218. .panel-date-label { color: #7bbef6; white-space: nowrap; }
  219. .panel-date-input { background: rgba(0,40,80,0.7); border: 1px solid rgba(0,212,255,0.25); border-radius: 3px; color: #e0fcff; font-weight: 300; padding: 0 6px; outline: none; cursor: pointer; }
  220. .panel-date-input::-webkit-calendar-picker-indicator { filter: invert(0.7); cursor: pointer; }
  221. .panel-close-btn { cursor: pointer; flex-shrink: 0; transition: opacity 0.15s; }
  222. .panel-close-btn:hover { opacity: 0.7; }
  223. </style>