[Bug] 折线图动态加载数据时,设置tooltip无法正常显示,设置了多表联合 同步更新的时候
- Dominant language
- TypeScript
- Stars
- 67.3k
- Forks
- 19.8k
- Avg merge
- 11d 14h
- Merged PRs (30d)
- 8
Description
### Version
6.0.0
### Link to Minimal Reproduction
下面贴源码
### Steps to Reproduce
确定
重置
{{ title }}
{{ title }}
A
{{ item.name }}
{{ item.value }}
B
{{ item.name }}
{{ item.value }}
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 = `<div style="font-weight:bold; margin-bottom:4px;">${time}</div>`;
params.forEach(p => {
content += `<div style="color:${p.color}">${p.seriesName}: ${p.data[1]}</div>`;
});
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 = [];
});
#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;
}
### Current Behavior
动态添加数据后无法显示tooltip
### Expected Behavior
动态添加数据后正常显示tooltip,并且是联动显示
### Environment
```markdown
- OS:win11
- Browser:Chrome 96.0.4664.55
- Framework:Vue@3
```
### Any additional comments?
主要是在setoption方法上的问题
Contributor guide
Assessment
This issue has not been assessed yet.