|
|
@@ -0,0 +1,361 @@
|
|
|
+<template>
|
|
|
+ <div class="fiber-temp-chart-wrap">
|
|
|
+ <div class="chart-header">
|
|
|
+ <div class="chart-title">{{ title }}</div>
|
|
|
+ <div class="chart-header-desc">
|
|
|
+ <div class="desc-item">
|
|
|
+ <span class="color-block normal"></span>
|
|
|
+ <span class="text">正常</span>
|
|
|
+ </div>
|
|
|
+ <div class="desc-item">
|
|
|
+ <span class="color-block alarm"></span>
|
|
|
+ <span class="text">报警</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="chart-container" ref="chartDomRef"></div>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup>
|
|
|
+import { ref, watch, onMounted, onBeforeUnmount, nextTick } from 'vue';
|
|
|
+import * as echarts from 'echarts';
|
|
|
+import firePng from '@/assets/images/fire.png';
|
|
|
+import warningSVG from '@/assets/images/warning.svg';
|
|
|
+
|
|
|
+const props = defineProps({
|
|
|
+ xAxisData: {
|
|
|
+ type: Array,
|
|
|
+ default: () => [],
|
|
|
+ },
|
|
|
+ seriesData: {
|
|
|
+ type: Array,
|
|
|
+ default: () => [],
|
|
|
+ },
|
|
|
+ maxValue: {
|
|
|
+ type: Number,
|
|
|
+ default: 60,
|
|
|
+ },
|
|
|
+ minValue: {
|
|
|
+ type: Number,
|
|
|
+ default: 20,
|
|
|
+ },
|
|
|
+ height: {
|
|
|
+ type: String,
|
|
|
+ default: '440px',
|
|
|
+ },
|
|
|
+ title: {
|
|
|
+ type: String,
|
|
|
+ default: '光纤测温',
|
|
|
+ },
|
|
|
+});
|
|
|
+
|
|
|
+const emit = defineEmits(['chart-click']);
|
|
|
+
|
|
|
+const chartDomRef = ref(null);
|
|
|
+let chartInstance = null;
|
|
|
+
|
|
|
+function disposeChart() {
|
|
|
+ if (chartInstance) {
|
|
|
+ chartInstance.dispose();
|
|
|
+ chartInstance = null;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function initChart() {
|
|
|
+ if (!chartDomRef.value) return;
|
|
|
+ disposeChart();
|
|
|
+ chartInstance = echarts.init(chartDomRef.value);
|
|
|
+ renderOption();
|
|
|
+ chartInstance.on('click', (params) => {
|
|
|
+ emit('chart-click', params);
|
|
|
+ });
|
|
|
+ window.addEventListener('resize', handleResize);
|
|
|
+}
|
|
|
+
|
|
|
+function handleResize() {
|
|
|
+ chartInstance?.resize();
|
|
|
+}
|
|
|
+
|
|
|
+function getMarkPointData() {
|
|
|
+ const markPoints = [];
|
|
|
+ const { seriesData, minValue, maxValue } = props;
|
|
|
+ seriesData.forEach((val, idx) => {
|
|
|
+ if (val < minValue || val >= maxValue) {
|
|
|
+ markPoints.push({
|
|
|
+ coord: [idx, val + 15],
|
|
|
+ symbol: `image://${val < minValue ? warningSVG : firePng}`,
|
|
|
+ symbolSize: [60, 60],
|
|
|
+ tooltip: {
|
|
|
+ formatter: `<div style="color:#ff4422;font-weight:bold">异常确认<br/>立即排查</div>`,
|
|
|
+ },
|
|
|
+ });
|
|
|
+ }
|
|
|
+ });
|
|
|
+ return markPoints;
|
|
|
+}
|
|
|
+
|
|
|
+// 获取报警区域的连续数据(包含边界点)
|
|
|
+function getAlarmContinuousData(data, minValue, maxValue) {
|
|
|
+ const result = [];
|
|
|
+ const n = data.length;
|
|
|
+
|
|
|
+ const isAlarm = (val) => val < minValue || val >= maxValue;
|
|
|
+
|
|
|
+ for (let i = 0; i < n; i++) {
|
|
|
+ const val = data[i];
|
|
|
+ if (isAlarm(val)) {
|
|
|
+ // 当前点是报警点,保留原值
|
|
|
+ result.push(val);
|
|
|
+ } else {
|
|
|
+ // 检查相邻点是否有报警点,如果有则保留当前值作为边界
|
|
|
+ const prevAlarm = i > 0 && isAlarm(data[i - 1]);
|
|
|
+ const nextAlarm = i < n - 1 && isAlarm(data[i + 1]);
|
|
|
+
|
|
|
+ if (prevAlarm || nextAlarm) {
|
|
|
+ // 使用实际边界值,与正常折线在切换点保持连续
|
|
|
+ result.push(val);
|
|
|
+ } else {
|
|
|
+ result.push(null);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return result;
|
|
|
+}
|
|
|
+
|
|
|
+function getAlarmPointData(data, minValue, maxValue) {
|
|
|
+ return data.map((val) => (val < minValue || val >= maxValue ? val : null));
|
|
|
+}
|
|
|
+
|
|
|
+function renderOption() {
|
|
|
+ if (!chartInstance) return;
|
|
|
+ const { xAxisData, seriesData, minValue, maxValue } = props;
|
|
|
+
|
|
|
+ // 报警填充数据:包含实际边界点,使报警区域连续
|
|
|
+ const alarmFillData = getAlarmContinuousData(seriesData, minValue, maxValue);
|
|
|
+
|
|
|
+ // 报警折线数据:只保留超过上限或低于下限的点
|
|
|
+ const alarmLineData = getAlarmPointData(seriesData, minValue, maxValue);
|
|
|
+
|
|
|
+ // 正常填充数据:仅保留上下限之间的值,报警值设为 null
|
|
|
+ const normalFillData = seriesData.map((val) => (val >= minValue && val < maxValue ? val : null));
|
|
|
+
|
|
|
+ // 正常折线数据:只保留上下限之间的点
|
|
|
+ const normalLineData = seriesData.map((val) => (val >= minValue && val < maxValue ? val : null));
|
|
|
+
|
|
|
+ const option = {
|
|
|
+ backgroundColor: 'transparent',
|
|
|
+ title: { show: false },
|
|
|
+ tooltip: {
|
|
|
+ trigger: 'axis',
|
|
|
+ backgroundColor: 'rgba(0,20,40,0.85)',
|
|
|
+ borderColor: '#2ef3ef',
|
|
|
+ textStyle: { color: '#fff' },
|
|
|
+ formatter: function (params) {
|
|
|
+ const dataIndex = params[0]?.dataIndex;
|
|
|
+ if (dataIndex !== undefined && seriesData[dataIndex] !== undefined) {
|
|
|
+ const val = seriesData[dataIndex];
|
|
|
+ const isAlarm = val < minValue || val >= maxValue;
|
|
|
+ return `<div style="font-weight:bold;${isAlarm ? 'color:#ff8800' : 'color:#35d9e5'}">
|
|
|
+ ${xAxisData[dataIndex] || ''}<br/>
|
|
|
+ 温度:${val}℃${isAlarm ? ' 超限' : ''}
|
|
|
+ </div>`;
|
|
|
+ }
|
|
|
+ return '';
|
|
|
+ },
|
|
|
+ },
|
|
|
+ legend: { show: false },
|
|
|
+ grid: {
|
|
|
+ left: '4%',
|
|
|
+ right: '3%',
|
|
|
+ top: '18%',
|
|
|
+ bottom: '18%',
|
|
|
+ },
|
|
|
+ xAxis: {
|
|
|
+ type: 'category',
|
|
|
+ data: xAxisData,
|
|
|
+ axisLine: { lineStyle: { color: '#2ef3ef' } },
|
|
|
+ axisLabel: { color: '#ffffff', fontSize: 14 },
|
|
|
+ splitLine: { show: true, lineStyle: { color: 'rgba(46,243,239,0.15)' } },
|
|
|
+ },
|
|
|
+ yAxis: {
|
|
|
+ type: 'value',
|
|
|
+ name: '温度℃',
|
|
|
+ nameTextStyle: { color: '#fff' },
|
|
|
+ min: 0,
|
|
|
+ max: 130,
|
|
|
+ axisLine: { lineStyle: { color: '#2ef3ef' } },
|
|
|
+ axisLabel: { color: '#ffffff' },
|
|
|
+ splitLine: { lineStyle: { color: 'rgba(46,243,239,0.15)' } },
|
|
|
+ },
|
|
|
+ dataZoom: [
|
|
|
+ {
|
|
|
+ type: 'slider',
|
|
|
+ show: true,
|
|
|
+ height: 20,
|
|
|
+ bottom: 8,
|
|
|
+ backgroundColor: '#152c47',
|
|
|
+ fillerColor: '#2ef3ef66',
|
|
|
+ handleStyle: { color: '#2ef3ef' },
|
|
|
+ textStyle: { color: '#ffffff' },
|
|
|
+ start: 0,
|
|
|
+ end: 30,
|
|
|
+ zoomLock: false,
|
|
|
+ },
|
|
|
+ { type: 'inside' },
|
|
|
+ ],
|
|
|
+ series: [
|
|
|
+ // 正常填充(青色)
|
|
|
+ {
|
|
|
+ name: '正常填充',
|
|
|
+ type: 'line',
|
|
|
+ smooth: true,
|
|
|
+ data: normalFillData,
|
|
|
+ symbol: 'none',
|
|
|
+ connectNulls: false,
|
|
|
+ lineStyle: { width: 0 },
|
|
|
+ tooltip: { show: false },
|
|
|
+ areaStyle: {
|
|
|
+ color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
|
|
+ { offset: 0, color: 'rgba(53,217,229,0.35)' },
|
|
|
+ { offset: 1, color: 'rgba(53,217,229,0.05)' },
|
|
|
+ ]),
|
|
|
+ },
|
|
|
+ z: 1,
|
|
|
+ },
|
|
|
+ // 报警填充(橙色)- 包含边界点,形成连续填充
|
|
|
+ {
|
|
|
+ name: '报警填充',
|
|
|
+ type: 'line',
|
|
|
+ smooth: true,
|
|
|
+ data: alarmFillData,
|
|
|
+ symbol: 'none',
|
|
|
+ connectNulls: false,
|
|
|
+ lineStyle: { width: 0 },
|
|
|
+ tooltip: { show: false },
|
|
|
+ areaStyle: {
|
|
|
+ color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
|
|
+ { offset: 0, color: 'rgba(255,136,0,0.5)' },
|
|
|
+ { offset: 1, color: 'rgba(255,136,0,0.05)' },
|
|
|
+ ]),
|
|
|
+ },
|
|
|
+ z: 2,
|
|
|
+ },
|
|
|
+ // 正常折线(青色)
|
|
|
+ {
|
|
|
+ name: '正常测温',
|
|
|
+ type: 'line',
|
|
|
+ smooth: true,
|
|
|
+ data: normalLineData,
|
|
|
+ symbol: 'circle',
|
|
|
+ symbolSize: 8,
|
|
|
+ connectNulls: false,
|
|
|
+ lineStyle: { width: 3, color: '#35d9e5' },
|
|
|
+ itemStyle: { color: '#35d9e5' },
|
|
|
+ z: 3,
|
|
|
+ },
|
|
|
+ // 报警折线(橙色)- 包含边界点,形成连续折线
|
|
|
+ {
|
|
|
+ name: '报警测温',
|
|
|
+ type: 'line',
|
|
|
+ smooth: true,
|
|
|
+ data: alarmLineData,
|
|
|
+ symbol: 'circle',
|
|
|
+ symbolSize: 10,
|
|
|
+ connectNulls: false,
|
|
|
+ lineStyle: { width: 3, color: '#ff8800' },
|
|
|
+ itemStyle: {
|
|
|
+ color: function (params) {
|
|
|
+ const val = params.data;
|
|
|
+ // 边界点(等于阈值)不显示圆点
|
|
|
+ if (val === minValue || val === maxValue) return 'transparent';
|
|
|
+ return '#ff8800';
|
|
|
+ },
|
|
|
+ },
|
|
|
+ markPoint: {
|
|
|
+ data: getMarkPointData(),
|
|
|
+ offset: [0, -100],
|
|
|
+ },
|
|
|
+ markLine: {
|
|
|
+ silent: true,
|
|
|
+ lineStyle: { type: 'dashed', width: 2, color: '#ff2222' },
|
|
|
+ label: { show: true, position: 'end', fontSize: 12, color: '#ff2222' },
|
|
|
+ data: [
|
|
|
+ { yAxis: minValue, label: { formatter: '下限阈值' } },
|
|
|
+ { yAxis: maxValue, label: { formatter: '上限阈值' } },
|
|
|
+ ],
|
|
|
+ },
|
|
|
+ z: 4,
|
|
|
+ },
|
|
|
+ ],
|
|
|
+ };
|
|
|
+
|
|
|
+ chartInstance.setOption(option, true);
|
|
|
+}
|
|
|
+
|
|
|
+watch(
|
|
|
+ () => [props.xAxisData, props.seriesData, props.minValue, props.maxValue],
|
|
|
+ () => {
|
|
|
+ nextTick(() => renderOption());
|
|
|
+ },
|
|
|
+ { deep: true }
|
|
|
+);
|
|
|
+
|
|
|
+onMounted(() => {
|
|
|
+ nextTick(() => initChart());
|
|
|
+});
|
|
|
+
|
|
|
+onBeforeUnmount(() => {
|
|
|
+ disposeChart();
|
|
|
+ window.removeEventListener('resize', handleResize);
|
|
|
+});
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.fiber-temp-chart-wrap {
|
|
|
+ width: 100%;
|
|
|
+ height: v-bind(height);
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+}
|
|
|
+.chart-header {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ padding: 10px 16px;
|
|
|
+}
|
|
|
+.chart-title {
|
|
|
+ color: #ffffff;
|
|
|
+ font-size: 24px;
|
|
|
+ font-weight: bold;
|
|
|
+}
|
|
|
+.chart-header-desc {
|
|
|
+ display: flex;
|
|
|
+ gap: 32px;
|
|
|
+ align-items: center;
|
|
|
+}
|
|
|
+.desc-item {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+}
|
|
|
+.color-block {
|
|
|
+ width: 32px;
|
|
|
+ height: 12px;
|
|
|
+ border-radius: 2px;
|
|
|
+}
|
|
|
+.color-block.normal {
|
|
|
+ background-color: #35d9e5;
|
|
|
+}
|
|
|
+.color-block.alarm {
|
|
|
+ background-color: #ff8800;
|
|
|
+}
|
|
|
+.text {
|
|
|
+ color: #ffffff;
|
|
|
+ font-size: 16px;
|
|
|
+}
|
|
|
+.chart-container {
|
|
|
+ flex: 1;
|
|
|
+ width: 100%;
|
|
|
+}
|
|
|
+</style>
|