Skip to content

[Bug] 折线图动态加载数据时,设置tooltip无法正常显示,设置了多表联合 同步更新的时候 #21310

@ZH-GL

Description

@ZH-GL

Version

6.0.0

Link to Minimal Reproduction

下面贴源码

Steps to Reproduce

数据类型:
槽号:
时间选择:
确定 重置
<!-- 主容器:树在左、图在中、右侧数据列 -->
<div class="main-container">
  <div id="treebox" class="tree-container">
    <a-tree v-model:expandedKeys="expandedKeys" v-model:selectedKeys="selectedKeys"
      v-model:checkedKeys="checkedKeys" :tree-data="treeData" @select="onTreeSelect">
      <template #title="{ title, key }">
        <span v-if="key === '0-0-1-0'" style="color: #1890ff">{{ title }}</span>
        <template v-else>{{ title }}</template>
      </template>
    </a-tree>
  </div>

  <div ref="chartRef" class="chart-container"></div>

  <div class="data-columns">
    <div class="column">
      <h3>A</h3>
      <div v-for="(item, index) in dataColumnA" :key="index" class="data-item">
        <span class="data-name">{{ item.name }}</span>
        <span class="data-value">{{ item.value }}</span>
      </div>
    </div>

    <div class="column">
      <h3>B</h3>
      <div v-for="(item, index) in dataColumnB" :key="index" class="data-item">
        <span class="data-name">{{ item.name }}</span>
        <span class="data-value">{{ item.value }}</span>
      </div>
    </div>
  </div>
</div>
<script setup> import { ref, onMounted, onBeforeUnmount, nextTick } from 'vue'; import * as echarts from 'echarts'; import locale from 'ant-design-vue/es/locale/zh_CN'; import axios from 'axios'; import dayjs from 'dayjs'; import 'dayjs/locale/zh-cn'; import { message } from 'ant-design-vue'; import Config from '@/config/Config'; dayjs.locale('zh-cn'); // ---------- 查询条件 ---------- const typeOptions = [ { value: '实时数据', label: '实时数据' }, { value: '历史数据', label: '历史数据' }, ]; const typeValue = ref('实时数据'); const areaValue = ref(2101); const dateValue = ref(); const localeRef = locale; const sureQuery = async () => { if (typeValue.value === '实时数据') { await initRealtime(); startWebSocket(); } else { if (!dateValue.value) { message.warn("请选择时间!"); return; } if (ws) { ws.close(); ws = null; } await fetchHistoryAndInit(); } }; const cancelQuery = () => { typeValue.value = '实时数据'; areaValue.value = 2301; dateValue.value = []; apiData.value = []; latestPoint.value = null; clearChartContainers(); updateRightColumnsFromLatest(); }; const dateChange = (d) => { dateValue.value = d; console.log(dateValue.value.format("YYYY-MM-DD 00:00:00")); }; // ---------- 树 ---------- const generateList = (prefix, keyPrefix) => Array.from({ length: 36 }, (_, index) => ({ title: `${prefix}${String(index + 1).padStart(2, '0')}`, key: `${keyPrefix}-${index}`, })); const lists = Array.from({ length: 8 }, (_, i) => generateList(`2${i + 1}`, `0-0-${i}`)); const treeData = ref([ { title: '原料二厂', key: '0-0', children: Array.from({ length: 8 }, (_, index) => ({ title: `${index + 1}工区`, key: `0-0-${index}`, children: lists[index], })), }, ]); const expandedKeys = ref(['0-0']); const selectedKeys = ref([]); const checkedKeys = ref([]); function onTreeSelect(selected, { node }) { if (!node.children || node.children.length === 0) { areaValue.value = Number(node.title); selectedKeys.value = [node.key]; } } // ---------- 图表 ---------- const chartRef = ref(null); const charts = ref([]); const MAX_POINTS_KEEP = 120; const lastTimestamps = ref({}); // 记录每个系列的最后时间戳 const _matrixDimensionData = { x: ['A', 'B'], y: [ { value: '电压' }, { value: '01~03' }, { value: '04~06' }, { value: '07~09' }, { value: '10~12' }, ], }; const lineColors = ['#5470C6', '#91CC75', '#EE6666']; const lineNames = ['线1', '线2', '线3']; const txtName = [ [], ['1', '2', '3'], ['4', '5', '6'], ['7', '8', '9'], ['10', '11', '12'] ]; function mapSeriesToKey(xidx, yidx, lineIndex) { if (yidx === 0) return xidx === 0 ? 'a01' : 'b01'; const offset = (yidx - 1) * 3 + lineIndex + 1; return (xidx === 0 ? 'a' : 'b') + String(offset + 1 - 1).padStart(2, '0'); } function parseTimeToMs(str) { const time = dayjs(str); if (!time.isValid()) { console.error('无效时间戳:', str); return null; // 返回 null 表示无效 } return time.valueOf(); } const apiData = ref([]); const latestPoint = ref(null); async function initRealtime() { try { const res = await axios.post(`${Config.Index}/api/iotdb/query/now/data/graph`, { start_time: '2025-09-20 00:00:00', end_time: '2025-09-30 00:00:00', mt_id: areaValue.value, point: 'point', }); apiData.value = res.data.data || []; await nextTick(); renderMatrixCharts(); updateRightColumnsFromLatest(); } catch (error) { console.error('initRealtime error:', error); } } async function fetchHistoryAndInit() { try { const res = await axios.post(`${Config.Index}api/iotdb/query/history/data/graph`, { start_time: dateValue.value.format("YYYY-MM-DD 00:00:00"), end_time: dateValue.value.format("YYYY-MM-DD 24:00:00"), mt_id: areaValue.value, point: "point", }); console.log(res); apiData.value = res.data.data || []; await nextTick(); renderMatrixCharts(); updateRightColumnsFromLatest(); } catch (error) { console.error('Fetch error:', error); } } function buildSeriesDataFor(xidx, yidx, lineIndex) { if (!apiData.value || !apiData.value.length) return []; const arr = []; // ✅ 电压行:固定 a01 / b01 if (yidx === 0) { const key = xidx === 0 ? 'a01' : 'b01'; for (const r of apiData.value) arr.push([parseTimeToMs(r.time), r[key] ?? 0]); return arr; } // ✅ 行 1(01~03),第一列(A01/B01)复用电压 if (yidx === 1 && lineIndex === 0) { const key = xidx === 0 ? 'a01' : 'b01'; for (const r of apiData.value) arr.push([parseTimeToMs(r.time), r[key] ?? 0]); return arr; } // ✅ 其他正常递增,从 a02/b02 开始 const baseIndex = (yidx - 1) * 3 + lineIndex + 1; // 改成 +1 而不是 +2 const key = (xidx === 0 ? 'a' : 'b') + String(baseIndex).padStart(2, '0'); for (const r of apiData.value) arr.push([parseTimeToMs(r.time), r[key] ?? 0]); return arr; } const MAX_TOOLTIP_POINTS = 5000; // 超过这个点数就不显示 tooltip function buildSeriesForCell(xidx, yidx) { const series = []; const xval = _matrixDimensionData.x[xidx]; const isRealtime = typeValue.value === '实时数据'; if (yidx === 0) { series.push({ name: `${xval} 电压`, type: 'line', showSymbol: false, sampling: isRealtime ? undefined : 'lttb', large: isRealtime ? false : true, progressive: 50000, // 每批绘制 5000 点 lineStyle: { width: 1, color: lineColors[0] }, color: lineColors[0], data: buildSeriesDataFor(xidx, yidx, 0), }); } else { for (let li = 0; li < lineNames.length; li++) { const legendName = xidx === 0 ? `A${txtName[yidx][li]}` : `B${txtName[yidx][li]}`; series.push({ name: legendName, type: 'line', showSymbol: false, sampling: isRealtime ? undefined : 'lttb', large: isRealtime ? false : true, lineStyle: { width: 1, color: lineColors[li] }, color: lineColors[li], data: buildSeriesDataFor(xidx, yidx, li), }); } } return series; } function clearChartContainers() { charts.value.forEach(c => c.dispose && c.dispose()); charts.value = []; if (chartRef.value) chartRef.value.innerHTML = ''; } function renderMatrixCharts() { clearChartContainers(); if (!chartRef.value) return; const rows = _matrixDimensionData.y.length; const cols = _matrixDimensionData.x.length; for (let yidx = 0; yidx < rows; yidx++) { for (let xidx = 0; xidx < cols; xidx++) { const wrapper = document.createElement('div'); wrapper.style.position = 'absolute'; wrapper.style.left = `${xidx * (100 / cols)}%`; wrapper.style.top = `${yidx * (100 / rows)}%`; wrapper.style.width = `${100 / cols}%`; wrapper.style.height = `${100 / rows}%`; wrapper.style.padding = '6px'; wrapper.style.boxSizing = 'border-box'; chartRef.value.appendChild(wrapper); const chart = echarts.init(wrapper); chart.group = 'matrixGroup'; const seriesData = buildSeriesForCell(xidx, yidx); chart.setOption({ tooltip: { trigger: 'axis', axisPointer: { type: 'line', snap: true }, confine: true, order: 'none', // 提高 tooltip 响应性 // position: (point) => [point[0] + 10, point[1] + 10], formatter: (params) => { console.log('收到参数:', params); if (!params || !params.length) return ''; const time = dayjs(params[0].axisValue).format('YYYY-MM-DD HH:mm:ss'); let content = `
${time}
`; params.forEach(p => { content += `
${p.seriesName}: ${p.data[1]}
`; }); return content; } }, legend: { data: seriesData.map(s => s.name), top: -5, textStyle: { fontSize: 10 }, itemWidth: 12, itemHeight: 8, type: 'scroll', }, grid: { left: 40, right: 20, top: 20, bottom: 10 }, xAxis: { type: 'time', axisLabel: { formatter: v => dayjs(v).format('HH:mm:ss') } }, yAxis: { scale: true }, dataZoom: [{ type: 'inside', xAxisIndex: 0 }], series: seriesData, }); charts.value.push(chart); } } echarts.connect(charts.value); setTimeout(() => charts.value.forEach(c => c.resize()), 50); } // ---------- WebSocket ---------- let ws = null; function startWebSocket() { if (ws) { ws.close(); ws = null; } const wsUrl = `${Config.wsUrl}data?mt_id=${areaValue.value}`; ws = new WebSocket(wsUrl); let lastUpdateTime = 0; // 节流控制 ws.onmessage = (evt) => { try { const now = Date.now(); if (now - lastUpdateTime < 200) return; // 节流 lastUpdateTime = now; const arr = JSON.parse(evt.data); if (!arr.length) return; const newest = Array.isArray(arr) ? arr[arr.length - 1] : arr; latestPoint.value = newest; const currentTime = parseTimeToMs(newest.time); if (currentTime === null) return; let idx = 0; for (let yidx = 0; yidx < _matrixDimensionData.y.length; yidx++) { for (let xidx = 0; xidx < _matrixDimensionData.x.length; xidx++) { const chart = charts.value[idx++]; if (!chart) continue; const seriesCount = (yidx === 0 ? 1 : 3); const curOpt = chart.getOption(); for (let si = 0; si < seriesCount; si++) { const key = mapSeriesToKey(xidx, yidx, si); const val = newest[key] ?? 0; const seriesKey = `${idx}-${si}`; // 跳过时间戳不递增的数据 if (lastTimestamps.value[seriesKey] && lastTimestamps.value[seriesKey] >= currentTime) continue; lastTimestamps.value[seriesKey] = currentTime; // ✅ 直接修改原 series.data 引用 const s = curOpt.series[si]; s.data.push([currentTime, val]); if (s.data.length > MAX_POINTS_KEEP) s.data.shift(); } // 更新 xAxis 范围 const allData = curOpt.series.flatMap(s => s.data); const minTime = allData.length > 0 ? allData[0][0] : currentTime - 60 * 1000; const maxTime = currentTime + 1000; // 只更新 xAxis 和 series,保留 tooltip/legend/grid 的引用 chart.setOption({ xAxis: { min: minTime, max: maxTime }, series: curOpt.series }, { notMerge: false, lazyUpdate: false }); } } updateRightColumnsFromLatest(); } catch (e) { console.error('WebSocket 数据处理错误:', e); } }; ws.onerror = (error) => { console.error('WebSocket 错误:', error); ws.close(); ws = null; setTimeout(startWebSocket, 5000); }; ws.onclose = () => { console.log('WebSocket 连接关闭'); ws = null; }; } // ---------- 右侧数据列 ---------- const dataColumnA = ref([]); const dataColumnB = ref([]); function updateRightColumnsFromLatest() { const src = latestPoint.value ?? (apiData.value.length ? apiData.value[apiData.value.length - 1] : null); if (!src) { dataColumnA.value = []; dataColumnB.value = []; return; } dataColumnA.value = []; dataColumnB.value = []; _matrixDimensionData.y.forEach((yItem, yidx) => { if (yidx === 0) { // ✅ 电压行 dataColumnA.value.push({ name: '电压', value: src.a01 ?? 0 }); dataColumnB.value.push({ name: '电压', value: src.b01 ?? 0 }); } else { const names = txtName[yidx]; names.forEach((name, li) => { let keyA, keyB; // ✅ A01 / B01 复用电压 if (yidx === 1 && li === 0) { keyA = 'a01'; keyB = 'b01'; } else { const baseIndex = (yidx - 1) * 3 + li + 1; // 改为 +1,保持与上方一致 keyA = 'a' + String(baseIndex).padStart(2, '0'); keyB = 'b' + String(baseIndex).padStart(2, '0'); } dataColumnA.value.push({ name: `A${name}`, value: src[keyA] ?? 0 }); dataColumnB.value.push({ name: `B${name}`, value: src[keyB] ?? 0 }); }); } }); } // ---------- 生命周期 ---------- onMounted(async () => { nextTick(() => renderMatrixCharts()); }); onBeforeUnmount(() => { if (ws) { ws.close(); ws = null; } charts.value.forEach(c => c.dispose()); charts.value = []; }); </script> <style scoped> #QueryCard { border-radius: 8px; background: linear-gradient(135deg, #ffffff, #f6f8fa); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); padding: 1vh; font-size: 1.6vh; cursor: default; } .Card_Item { width: 90%; margin: 1vh auto; } .QueryCard_Item { display: flex; align-items: center; justify-content: center; gap: 1.5vw; } .query-item { display: flex; align-items: center; gap: 0.5vw; } .query-item span { font-weight: 500; color: #333; } #search { background: linear-gradient(45deg, #399bff, #5abaff); transition: transform 0.2s, box-shadow 0.2s; } #cancel { background: linear-gradient(45deg, #efca04, #ffdb4d); transition: transform 0.2s, box-shadow 0.2s; } .Button_Item { color: #fff; border-radius: 6px; border: none; width: 4vw; height: 3.5vh; letter-spacing: 0.2vw; font-size: 0.8vw; font-weight: 500; cursor: pointer; } .Button_Item:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); } .main-container { display: flex; width: 100%; height: 79vh; border-radius: 8px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); overflow: visible; } .tree-container { width: 12vw; min-width: 150px; max-width: 200px; padding: 1vh; background: #fafafa; border-right: 1px solid #e8e8e8; overflow: auto; } .chart-container { flex: 1; position: relative; height: 100%; background: #fff; } .data-columns { width: 14vw; min-width: 180px; max-width: 220px; padding: 1.5vh; background: linear-gradient(#fff, #f9f9f9); border-left: 1px solid #e8e8e8; display: flex; gap: 1rem; box-sizing: border-box; } .column { flex: 1; display: flex; flex-direction: column; align-items: center; } .data-item { width: 100%; display: flex; justify-content: space-between; padding: 0.5rem; background: #fff; border-radius: 6px; margin-bottom: 6px; } </style>

Current Behavior

动态添加数据后无法显示tooltip

Expected Behavior

动态添加数据后正常显示tooltip,并且是联动显示

Environment

- OS:win11
- Browser:Chrome 96.0.4664.55
- Framework:Vue@3

Any additional comments?

主要是在setoption方法上的问题

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugpendingWe are not sure about whether this is a bug/new feature.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions