Files
taskq/x/monitor/ui/components/tasks-chart.js
hupeh 326f2a371c feat: 优化监控仪表盘 UI
- 添加 appbar 导航栏,支持 Chart/Queues 视图切换
- appbar 切换使用 history API,支持浏览器前进/后退
- 图表视图占满整个可视区域
- queue-modal 共享 appbar 样式
- 修复 queue tab count 字段名大小写问题
- tooltip 跟随鼠标显示在右下方,移除箭头
- 图表 canvas 鼠标样式改为准星
- pause/resume 队列后刷新列表
- example 添加 flag 配置参数
2025-12-10 00:53:30 +08:00

281 lines
8.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { LitElement, html, css } from 'lit';
import { Chart, registerables, Tooltip } from 'chart.js';
Chart.register(...registerables);
// 自定义 tooltip positioner显示在鼠标右下方
Tooltip.positioners.cursor = function (elements, eventPosition, tooltip) {
return {
x: eventPosition.x + 20,
y: eventPosition.y + 15,
xAlign: 'left',
yAlign: 'top'
};
};
// 十字准星 + 拖拽选择插件
const crosshairPlugin = {
id: 'crosshair',
afterEvent(chart, args) {
const event = args.event;
if (event.type === 'mousemove') {
chart.crosshair = { x: event.x, y: event.y };
} else if (event.type === 'mouseout') {
chart.crosshair = null;
}
},
afterDraw(chart) {
const ctx = chart.ctx;
const topY = chart.scales.y.top;
const bottomY = chart.scales.y.bottom;
const leftX = chart.scales.x.left;
const rightX = chart.scales.x.right;
// 绘制选择区域
if (chart.dragSelect && chart.dragSelect.startX !== null) {
const { startX, currentX } = chart.dragSelect;
const x1 = Math.max(leftX, Math.min(startX, currentX));
const x2 = Math.min(rightX, Math.max(startX, currentX));
ctx.save();
ctx.fillStyle = 'rgba(255, 193, 7, 0.2)';
ctx.fillRect(x1, topY, x2 - x1, bottomY - topY);
ctx.restore();
}
// 绘制十字准星
if (!chart.crosshair) return;
const { x, y } = chart.crosshair;
// 检查鼠标是否在图表区域内
if (x < leftX || x > rightX || y < topY || y > bottomY) return;
ctx.save();
ctx.setLineDash([4, 4]);
ctx.lineWidth = 1;
ctx.strokeStyle = 'rgba(255, 255, 255, 0.4)';
// 垂直线
ctx.beginPath();
ctx.moveTo(x, topY);
ctx.lineTo(x, bottomY);
ctx.stroke();
// 水平线跟随鼠标
ctx.beginPath();
ctx.moveTo(leftX, y);
ctx.lineTo(rightX, y);
ctx.stroke();
ctx.restore();
}
};
Chart.register(crosshairPlugin);
class TasksChart extends LitElement {
static properties = {
data: { type: Object },
timestamps: { type: Array }
};
static styles = css`
:host {
display: block;
width: 100%;
height: 100%;
}
canvas {
width: 100% !important;
height: 100% !important;
cursor: crosshair;
}
`;
constructor() {
super();
this.data = { labels: [], datasets: {} };
this.timestamps = [];
this.chart = null;
this.isDragging = false;
}
firstUpdated() {
this.initChart();
this.setupDragSelect();
}
setupDragSelect() {
const canvas = this.shadowRoot.querySelector('canvas');
if (!canvas) return;
canvas.addEventListener('mousedown', (e) => {
if (!this.chart) return;
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const leftX = this.chart.scales.x.left;
const rightX = this.chart.scales.x.right;
if (x >= leftX && x <= rightX) {
this.isDragging = true;
this.chart.dragSelect = { startX: x, currentX: x };
this.chart.update('none');
}
});
canvas.addEventListener('mousemove', (e) => {
if (!this.isDragging || !this.chart) return;
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
this.chart.dragSelect.currentX = x;
this.chart.update('none');
});
canvas.addEventListener('mouseup', (e) => {
if (!this.isDragging || !this.chart) return;
this.isDragging = false;
const { startX, currentX } = this.chart.dragSelect;
const leftX = this.chart.scales.x.left;
const rightX = this.chart.scales.x.right;
// 计算选择的时间范围
const x1 = Math.max(leftX, Math.min(startX, currentX));
const x2 = Math.min(rightX, Math.max(startX, currentX));
// 最小选择宽度检查
if (Math.abs(x2 - x1) > 10) {
const startIndex = Math.round((x1 - leftX) / (rightX - leftX) * (this.timestamps.length - 1));
const endIndex = Math.round((x2 - leftX) / (rightX - leftX) * (this.timestamps.length - 1));
if (startIndex >= 0 && endIndex < this.timestamps.length) {
const startTime = this.timestamps[startIndex];
const endTime = this.timestamps[endIndex];
this.dispatchEvent(new CustomEvent('time-range-select', {
detail: { start: startTime, end: endTime },
bubbles: true,
composed: true
}));
}
}
this.chart.dragSelect = null;
this.chart.update('none');
});
canvas.addEventListener('mouseleave', () => {
if (this.isDragging && this.chart) {
this.isDragging = false;
this.chart.dragSelect = null;
this.chart.update('none');
}
});
}
initChart() {
const canvas = this.shadowRoot.querySelector('canvas');
if (!canvas) return;
const ctx = canvas.getContext('2d');
this.chart = new Chart(ctx, {
type: 'line',
data: {
labels: [],
datasets: [
{ label: 'Active', data: [], borderColor: '#42a5f5', backgroundColor: 'transparent', fill: false, hidden: true },
{ label: 'Pending', data: [], borderColor: '#5c6bc0', backgroundColor: 'transparent', fill: false },
{ label: 'Scheduled', data: [], borderColor: '#ffa726', backgroundColor: 'transparent', fill: false, hidden: true },
{ label: 'Retry', data: [], borderColor: '#ef5350', backgroundColor: 'transparent', fill: false, hidden: true },
{ label: 'Archived', data: [], borderColor: '#ab47bc', backgroundColor: 'transparent', fill: false, hidden: true },
{ label: 'Completed', data: [], borderColor: '#66bb6a', backgroundColor: 'transparent', fill: false, hidden: true },
{ label: 'Succeeded', data: [], borderColor: '#81c784', backgroundColor: 'transparent', fill: false, borderDash: [5, 5], hidden: true },
{ label: 'Failed', data: [], borderColor: '#e57373', backgroundColor: 'transparent', fill: false, borderDash: [5, 5], hidden: true }
]
},
options: {
responsive: true,
maintainAspectRatio: false,
animation: { duration: 0 },
clip: false,
interaction: { mode: 'index', intersect: false },
hover: { mode: 'index', intersect: false },
plugins: {
legend: {
position: 'bottom',
labels: {
color: '#e0e0e0',
padding: 15,
usePointStyle: true,
pointStyle: 'circle'
}
},
tooltip: {
enabled: true,
backgroundColor: 'rgba(30, 30, 30, 0.9)',
titleColor: '#e0e0e0',
bodyColor: '#e0e0e0',
borderColor: '#616161',
borderWidth: 1,
padding: 10,
displayColors: true,
position: 'cursor',
caretSize: 0
}
},
scales: {
x: {
grid: { color: '#616161' },
ticks: { color: '#9e9e9e', maxTicksLimit: 10 }
},
y: {
grid: { color: '#616161' },
ticks: { color: '#9e9e9e' },
beginAtZero: true
}
},
elements: {
point: {
radius: 0,
hoverRadius: 5,
hitRadius: 10,
hoverBackgroundColor: (ctx) => ctx.dataset.borderColor,
hoverBorderColor: (ctx) => ctx.dataset.borderColor,
hoverBorderWidth: 0
},
line: { tension: 0.3, borderWidth: 1.5, spanGaps: false }
}
}
});
this.updateChart();
}
updated(changedProperties) {
if (changedProperties.has('data') && this.chart) {
this.updateChart();
}
}
updateChart() {
if (!this.chart || !this.data) return;
this.chart.data.labels = this.data.labels || [];
this.chart.data.datasets[0].data = this.data.datasets?.active || [];
this.chart.data.datasets[1].data = this.data.datasets?.pending || [];
this.chart.data.datasets[2].data = this.data.datasets?.scheduled || [];
this.chart.data.datasets[3].data = this.data.datasets?.retry || [];
this.chart.data.datasets[4].data = this.data.datasets?.archived || [];
this.chart.data.datasets[5].data = this.data.datasets?.completed || [];
this.chart.data.datasets[6].data = this.data.datasets?.succeeded || [];
this.chart.data.datasets[7].data = this.data.datasets?.failed || [];
this.chart.update('none');
}
render() {
return html`<canvas></canvas>`;
}
}
customElements.define('tasks-chart', TasksChart);